diff --git a/CapyKit/.editorconfig b/CapyKit/.editorconfig new file mode 100644 index 0000000..0e1204c --- /dev/null +++ b/CapyKit/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS8625: Cannot convert null literal to non-nullable reference type. +dotnet_diagnostic.CS8625.severity = none diff --git a/CapyKit/Attributes/EnumerationAttribute.cs b/CapyKit/Attributes/EnumerationAttribute.cs new file mode 100644 index 0000000..47b6eaa --- /dev/null +++ b/CapyKit/Attributes/EnumerationAttribute.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Attributes +{ + /// + /// Custom attribute class for decorating enumeration fields with additional data. + /// + /// + /// Generic type parameter allowing for arbitrary declarations and assignments of meaning. + /// + [AttributeUsage(AttributeTargets.Field)] + public abstract class EnumerationAttribute : Attribute + { + #region Properties + + /// + /// Initializes a new instance of the class with a + /// specified value. + /// + /// The value. + public T Value { get; private set; } + + #endregion + + #region Constructors + + /// Gets the value of the enumeration represented by this attribute. + /// + /// Initializes a new instance of the class with a + /// specified value. + /// + protected EnumerationAttribute(T value) + { + this.Value = value; + } + + #endregion + } +} diff --git a/CapyKit/Attributes/EnumerationDescriptionAttribute.cs b/CapyKit/Attributes/EnumerationDescriptionAttribute.cs new file mode 100644 index 0000000..4b626fc --- /dev/null +++ b/CapyKit/Attributes/EnumerationDescriptionAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Attributes +{ + /// An attribute class for decorating enumeration fields with a description. + /// + [AttributeUsage(AttributeTargets.Field)] + public class EnumerationDescriptionAttribute : EnumerationAttribute + { + /// + /// Initializes a new instance of the class with + /// the specified description. + /// + /// The description of the enumeration value. + public EnumerationDescriptionAttribute(string description) + : base(description) + { + // + } + } +} diff --git a/CapyKit/CapyEvent.cs b/CapyKit/CapyEvent.cs new file mode 100644 index 0000000..8745bfe --- /dev/null +++ b/CapyKit/CapyEvent.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using CapyKit.Extensions; + +namespace CapyKit +{ + /// + /// The CapyEventReporter class is responsible for managing event subscriptions and emissions within CapyKit. + /// + /// + /// Because consumers of CapyKit may have varied ways of handling logging, the provides + /// a way for subscribers to recieve events for various "events" within the library. These can be thought of as + /// a logging solution for CapyKit. + /// + public static class CapyEventReporter + { + #region Members + + /// + /// A dictionary storing event handlers and their corresponding origins for each subscription level. + /// + private static Dictionary> subscribers = new Dictionary>(); + + #endregion + + #region Methods + + /// + /// Subscribes the specified event handler to the event with the given subscription level and + /// origin. + /// + /// + /// If there is no existing list for the given subscription level, a new list is created and + /// added to the dictionary. + /// + /// The event handler to subscribe. + /// The severity level of the event to subscribe to. + /// + /// (Optional) The name of the method or class where the subscription is made. + /// + public static void Subscribe(CapyEventHandler callback, EventLevel subscriptionLevel, [CallerMemberName] string origin = null) + { + if (!subscribers[subscriptionLevel].Any()) + { + subscribers.Add(subscriptionLevel, new List<(CapyEventHandler Handler, string origin)>()); + } + + subscribers[subscriptionLevel].Add((callback, origin ?? "[Unknown]")); + } + + /// + /// Unsubscribes the specified event handler from the event with the given origin. + /// + /// The event handler to unsubscribe. + /// + /// The name of the method or class where the subscription was made. + /// + public static void Unsubscribe(CapyEventHandler callback, string origin) + { + foreach (var value in Enum.GetValues(typeof(EventLevel))) + { + if (value is EventLevel) + { + subscribers[(EventLevel)value].RemoveAll(c => c.Handler == callback && c.origin == origin); + } + } + } + + /// Emits an event with the given severity level, message, and method name. + /// + /// In order to allow for efficient calling member access via + /// , it is suggested that is defined explicitly for formatted messages. + /// + /// The severity level of the event. + /// The message describing the reason for the event. + /// + /// (Optional) The name of the method where the event was raised. String formatting for + /// is accepted. + /// + /// + /// A variable-length parameters list containing arguments for formatting the message. + /// + /// + /// CapyEventReporter.EmitEvent(EventLevel.Error, "Could not find the description for {0}.", + /// args: new[] { enumeration }); + /// + internal static void EmitEvent(EventLevel eventLevel, string message, [CallerMemberName] string method = null, params object[] args) + { + if (!subscribers.ContainsKey(eventLevel)) + { + return; + } + + var formattedMessage = string.Format(message, args); + + var capyEventArgs = new CapyEventArgs(eventLevel, formattedMessage, method); + + foreach (var subscriber in subscribers[eventLevel]) + { + subscriber.Handler(capyEventArgs); + } + } + + #endregion + } + + /// + /// A delegate representing an event handler that accepts a instance. + /// + /// The CapyEventArgs instance containing event data. + public delegate void CapyEventHandler(CapyEventArgs e); + + /// + /// The CapyEventArgs class represents an event argument instance with event level, message, and + /// method name information. + /// + public class CapyEventArgs : EventArgs + { + #region Properties + + /// + /// Gets the severity level of the event. + /// + public EventLevel Level { get; private set; } + + /// + /// Gets the message describing the reason for the event. + /// + public string Message { get; private set; } + + /// + /// Gets the name of the method where the event was raised. + /// + public string MethodName { get; private set; } + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the CapyEventArgs class with the specified event level, message, and method name. + /// + /// The severity level of the event. + /// A descriptive message explaining the reason for the event. + /// The name of the method where the event was raised. + public CapyEventArgs(EventLevel level, string message, string method = null) + { + this.Level = level; + this.Message = message; + this.MethodName = method ?? "[Unknown]"; + } + + #endregion + } + + + /// Enumeration representing different event level severity values. + public enum EventLevel + { + /// Represents a critical error that requires immediate attention. + Critical = 0, + /// Represents an error that prevents the normal execution of the application. + Error = 1, + /// Represents informational messages that provide useful context to the consumer. + Information = 2, + /// Represents detailed messages that are typically used for debugging purposes. + Debug = 3 + } +} diff --git a/CapyKit/CapyKit.csproj b/CapyKit/CapyKit.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/CapyKit/CapyKit.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/CapyKit/CapyKit.sln b/CapyKit/CapyKit.sln new file mode 100644 index 0000000..486de70 --- /dev/null +++ b/CapyKit/CapyKit.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34408.163 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CapyKit", "CapyKit.csproj", "{D1ACE10F-CBC8-4BA8-BB85-11DB4EEE5912}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE8D96CA-FC33-4F28-AF49-0E4AEC6D3FD9}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1ACE10F-CBC8-4BA8-BB85-11DB4EEE5912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1ACE10F-CBC8-4BA8-BB85-11DB4EEE5912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1ACE10F-CBC8-4BA8-BB85-11DB4EEE5912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1ACE10F-CBC8-4BA8-BB85-11DB4EEE5912}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF075174-308F-4D69-9160-C3CCE89EAD68} + EndGlobalSection +EndGlobal diff --git a/CapyKit/Extensions/EnumerationExtensions.cs b/CapyKit/Extensions/EnumerationExtensions.cs new file mode 100644 index 0000000..f7afccf --- /dev/null +++ b/CapyKit/Extensions/EnumerationExtensions.cs @@ -0,0 +1,93 @@ +using CapyKit.Attributes; +using CapyKit.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Extensions +{ + public static class EnumerationExtensions + { + #region Methods + + /// + /// A extension method that parses a string into an enumeration. + /// + /// Generic type parameter. + /// The enumeration to act on. + /// The value. + /// A T. + public static T Parse(this T enumeration, string value) where T : Enum + { + return (T)Enum.Parse(typeof(T), value); + } + + /// + /// A extension method that parses a string into an enumeration. + /// + /// Generic type parameter. + /// The enumeration to act on. + /// The string value of the . + /// True to ignore case. + /// A T. + public static T Parse(this T enumeration, string value, bool ignoreCase) where T : Enum + { + return (T)Enum.Parse(typeof(T), value, ignoreCase); + } + + /// + /// An extension method that gets an integer value representing the enumation. + /// + /// The enumeration to act on. + /// The integer value of the enumeration. + public static int GetValue(this Enum enumeration) + { + return (int)Convert.ChangeType(enumeration, TypeCode.Int32); + } + + /// An extension method that gets a name. + /// The enumeration to act on. + /// The name of the enumeration. + public static string GetName(this Enum enumeration) + { + return Enum.GetName(enumeration.GetType(), enumeration) ?? "[Unknown]"; + } + + /// An extension method that gets a human readable name. + /// The enumeration to act on. + /// The human readable name of the enumeration. + public static string GetPrettyName(this Enum enumeration) + { + return LanguageHelper.CamelCaseToHumanReadable(GetName(enumeration)); + } + + /// An extension method that gets a description. + /// The enumeration to act on. + /// + /// The description if available, otherwise the string representation of the enumeration. + /// + public static string GetDescription(this Enum enumeration) + { + var memInfo = enumeration.GetType().GetMember(enumeration.GetName()); + if (memInfo.Any()) + { + var attribute = memInfo.First().GetCustomAttribute(typeof(EnumerationDescriptionAttribute)) as EnumerationDescriptionAttribute; + if (attribute == null) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not find the description for {0}.", args: new[] { enumeration }); + } + else + { + return attribute.Value; + } + } + + return enumeration.ToString(); + } + + #endregion Methods + } +} diff --git a/CapyKit/Helpers/LanguageHelper.cs b/CapyKit/Helpers/LanguageHelper.cs new file mode 100644 index 0000000..ec68d91 --- /dev/null +++ b/CapyKit/Helpers/LanguageHelper.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace CapyKit.Helpers +{ + public class LanguageHelper + { + #region Methods + + /// Converts camel case text to human readable text. + /// The value. + /// A string in human readable format. + public static string CamelCaseToHumanReadable(string value) + { + var regex = new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); + + return regex.Replace(value, " "); + } + + #endregion + } +} diff --git a/CapyKit/Pool.cs b/CapyKit/Pool.cs new file mode 100644 index 0000000..f7eeceb --- /dev/null +++ b/CapyKit/Pool.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace CapyKit +{ + /// + /// A managed pool of resources. This class provides a thread-safe way to manage a collection of + /// objects of type . + /// + /// The type of objects to be managed by the pool. + public class Pool + { + #region Members + + /// The collection of pooled items. + private readonly ConcurrentBag> poolItemCollection; + + /// (Immutable) The number of items in the pool. + private readonly int poolSize; + + #endregion Members + + #region Constructors + + /// + /// Initializes a new instance of the class with the specified pool size. + /// + /// The size of the pool. + public Pool(int poolSize) + { + this.poolSize = poolSize; + this.poolItemCollection = new ConcurrentBag>(); + FillPoolItemCollection(poolSize); + } + + /// + /// Initializes a new instance of the class with the specified pool size + /// and constructor selector. + /// + /// The size of the pool. + /// + /// The constructor selector used to create new instances of . + /// + public Pool(int poolSize, Func constructorSelector) + { + this.poolSize = poolSize; + this.poolItemCollection = new ConcurrentBag>(); + FillPoolItemCollection(poolSize, constructorSelector); + } + + /// + /// Initializes a new instance of the class with the specified collection + /// of items. + /// + /// + /// The collection of items with which to seed the pool. + /// + public Pool(IEnumerable collection) + { + this.poolSize = collection.Count(); + this.poolItemCollection = new ConcurrentBag>(); + FillPoolItemCollection(collection); + } + + #endregion Constructors + + #region Methods + + /// + /// Initializes the pool with the specified number of items using the default constructor. + /// + /// The size of the pool. + private void FillPoolItemCollection(int poolSize) + { + FillPoolItemCollection(poolSize, () => default(T)); + } + + /// + /// Initializes the pool with the specified number of items using the specified constructor + /// selector. + /// + /// The size of the pool. + /// The constructor selector. + private void FillPoolItemCollection(int poolSize, Func constructorSelector) + { + for (int i = 0; i < poolSize; i++) + { + this.poolItemCollection.Add(new PoolItem(constructorSelector(), i)); + } + } + + /// Fill the pool item collection from an existing collection. + /// The collection. + private void FillPoolItemCollection(IEnumerable collection) + { + int index = 0; + foreach (var item in collection) + { + this.poolItemCollection.Add(new PoolItem(item, index++)); + } + } + + /// Gets the first available item from the pool and sets its lock. + /// The first available item from the pool. + public PoolItem GetAvailableItem() + { + lock (this.poolItemCollection) + { + if (this.poolItemCollection.Any(item => !item.Locked)) + { + var firstAvailableItem = this.poolItemCollection.First(item => !item.Locked); + firstAvailableItem.SetLock(); + + CapyEventReporter.EmitEvent(EventLevel.Debug, "Accessed ppol and retrieved {0}", args: new[] { firstAvailableItem }); + + return firstAvailableItem; + } + } + + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not return an available item."); + return null; + } + + /// Releases the lock on the specified item and returns it to the pool. + /// + /// This method sets the flag to so that + /// it can be retrieved by . + /// + /// The item to release. + public void ReleaseItem(PoolItem item) + { + item.ReleaseLock(); + } + + #endregion Methods + } + + /// A pool item. This class cannot be inherited. + /// The type of the pooled item. + public sealed class PoolItem + { + #region Members + + /// The pooled item. + private readonly T item; + + /// A flag indicating whether the item is locked or not. + private bool locked; + + /// The zero-based index of the pooled item. + private readonly int index; + + /// The name of the pooled item . + private readonly string typeName; + + #endregion Members + + #region Properties + + /// Gets the pooled resource. + /// The pooled resource. + public T Item + { + get + { + return this.item; + } + } + + /// Gets a value indicating whether this object is locked or not. + /// A value indicating whether this object is locked or not. + public bool Locked + { + get + { + return this.locked; + } + } + + /// Gets the zero-based index of the pooled item. + /// The index. + public int Index + { + get + { + return this.index; + } + } + + /// Gets the name of the of the pooled item. + /// The name of the of the pooled item. + public string TypeName + { + get + { + return this.typeName; + } + } + + #endregion Properties + + #region Constructors + + /// + /// Initializes a new instance of the class with the specified item and + /// index. + /// + /// The pooled item. + /// The zero-based index of the pooled item. + internal PoolItem(T item, int index) + { + this.item = item; + this.index = index; + this.locked = false; + this.typeName = typeof(T).Name; + } + + #endregion Constructors + + #region Methods + + /// Sets the lock on the item indicating that it is in use. + /// If the item is already locked, an error event is emitted. + /// + /// if the item is locked successfully, if it + /// fails. + /// + public bool SetLock() + { + if (this.locked) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Lock requested for {0}, but the lock request failed.", args: new[] { this }); + return false; + } + + this.locked = true; + + return true; + } + + /// Releases the lock on the item. + /// If the item is not locked, an error event is emitted. + public void ReleaseLock() + { + if (!this.locked) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Lock release requested for {0}, but the lock was already released.", args: new[] { this } ); + } + + this.locked = false; + } + + #endregion Methods + + #region Overrides + + /// Returns a string that represents the current object and its lock state. + /// A string that represents the current object and its lock state. + public override string ToString() + { + return string.Format("{0} {1} ({2})", this.typeName, this.index, this.locked ? "Locked" : "Unlocked"); + } + + #endregion Overrides + } +}