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>
/// Creates a new EntityList instance with no data.
/// </summary>
public EntityList() : this(null, false) { }
public EntityList() : this(null) { }
/// <summary>
/// Creates a new EntityList instance using the given JSON token as input.
/// </summary>
/// <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="ArgumentNullException">
/// 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">
/// When enforceTypes is set, this is thrown if an EntityName results in having its Type be Unspecified.
/// </exception>
public EntityList(JToken? input, bool enforceTypes) {
public EntityList(JToken? input) {
if (input == null) {
_innerList = new List<EntityName>().AsReadOnly();
return;
@ -50,8 +49,6 @@ public class EntityList : IEnumerable<EntityName> {
foreach (var item in inputArray.Values<string>()) {
if (string.IsNullOrWhiteSpace(item)) continue;
var itemName = new EntityName(item);
if (enforceTypes && itemName.Type == EntityType.Unspecified)
throw new FormatException($"The following value is not prefixed: {item}");
list.Add(itemName);
}
_innerList = list.AsReadOnly();
@ -82,7 +79,7 @@ public class EntityList : IEnumerable<EntityName> {
} else {
foreach (var r in authorRoles) {
if (!string.Equals(r.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(r.Id);
if (keepId) entry.Id = r.Id;
return true;
}
}
@ -91,7 +88,7 @@ public class EntityList : IEnumerable<EntityName> {
if (entry.Id.Value == channel.Id) return true;
} else {
if (!string.Equals(channel.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(channel.Id);
if (keepId) entry.Id = channel.Id;
return true;
}
} else { // User
@ -99,7 +96,7 @@ public class EntityList : IEnumerable<EntityName> {
if (entry.Id.Value == author.Id) return true;
} else {
if (!string.Equals(author.Username, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(author.Id);
if (keepId) entry.Id = author.Id;
return true;
}
}

View file

@ -6,23 +6,29 @@
/// </summary>
public class EntityName {
/// <summary>
/// The entity's type, if specified in configuration.
/// The entity's type, as specified in configuration.
/// </summary>
public EntityType Type { get; private set; }
private ulong? _id;
/// <summary>
/// Entity's unique ID value (snowflake). May be null if the value is not known.
/// </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>
/// Entity's name as specified in configuration. May be null if it was not specified.
/// This value is not updated during runtime.
/// </summary>
/// <remarks>This value is not updated during runtime.</remarks>
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>
/// Creates a new object instance from the given input string.
/// 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) {
if (!Id.HasValue) Id = id;
/// <summary>
/// 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>
/// Returns the appropriate prefix corresponding to an EntityType.
/// </summary>
public static char Prefix(EntityType t) => t switch {
public static char GetPrefix(EntityType t) => t switch {
EntityType.Role => '&',
EntityType.Channel => '#',
EntityType.User => '@',
@ -90,7 +105,7 @@ public class EntityName {
/// Returns a string representation of this item in proper EntityName format.
/// </summary>
public override string ToString() {
var pf = Prefix(Type);
var pf = GetPrefix(Type);
if (Id.HasValue && Name != null)
return $"{pf}{Id.Value}::{Name}";

View file

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

View file

@ -75,7 +75,7 @@ public class FilterList {
if (incoming.Type != JTokenType.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.
if (exemptKey != null) {
@ -85,7 +85,7 @@ public class FilterList {
} else if (incomingEx.Type != JTokenType.Array) {
throw new FormatException("Filtering exemption list must be a JSON array.");
} else {
FilterExemptions = new EntityList(incomingEx, true);
FilterExemptions = new EntityList(incomingEx);
}
} else {
FilterExemptions = new EntityList();

View file

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

View file

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

View file

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

View file

@ -5,11 +5,13 @@ class ModuleConfig {
public EntityName Role { get; }
public ModuleConfig(JObject conf) {
var cfgRole = conf[nameof(Role)]?.Value<string>();
if (string.IsNullOrWhiteSpace(cfgRole))
throw new ModuleLoadException("Role was not specified.");
Role = new EntityName(cfgRole);
if (Role.Type != EntityType.Role)
throw new ModuleLoadException("Name specified in configuration is not a role.");
try {
Role = new EntityName(conf[nameof(Role)]?.Value<string>()!, EntityType.Role);
} catch (ArgumentException) {
throw new ModuleLoadException("Role was not properly specified.");
} catch (FormatException) {
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>();
if (rptch != null) {
ReportingChannel = new EntityName(rptch);
if (ReportingChannel.Type != EntityType.Channel)
try {
ReportingChannel = new EntityName(rptch, EntityType.Channel);
} catch (FormatException) {
throw new ModuleLoadException($"'{nameof(ReportingChannel)}' is not defined as a channel{errpostfx}");
}
}
// Regex loading

View file

@ -160,14 +160,14 @@ class ResponseExecutor {
SocketGuildUser? tuser;
SocketRole? trole;
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);
else {
if (userName.Name == "_") tuser = _user;
else tuser = userName.FindUserIn(_guild);
}
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);
else trole = roleName.FindRoleIn(_guild);
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>();
foreach (var item in config.Properties()) {
var name = new EntityName(item.Name);
if (name.Type != EntityType.Role) throw new ModuleLoadException($"'{item.Name}' is not specified as a role.");
EntityName name;
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);
if (role == null) throw new ModuleLoadException($"Unable to find role '{name}'.");

View file

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