commit 8ac380c7d4e7def38668aa24f64ac4f3197a2fda Author: Maksym Sadovnychyy Date: Tue Sep 3 18:38:52 2024 +0200 (feature): init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fa7210 --- /dev/null +++ b/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs +.directory + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..1011c13 --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# MaksIT.Core + +MaksIT.Core is a collection of helper methods and extensions for .NET projects, designed to simplify common tasks and improve code readability. The library includes extensions for `Guid`, `string`, `Object`, and a base class for creating enumeration types. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) + - [Enumeration](#enumeration) + - [Guid Extensions](#guid-extensions) + - [Object Extensions](#object-extensions) + - [String Extensions](#string-extensions) +- [Available Methods](#available-methods) + - [Enumeration Methods](#enumeration-methods) + - [Guid Methods](#guid-methods) + - [Object Methods](#object-methods) + - [String Methods](#string-methods) +- [Contributing](#contributing) +- [License](#license) + +## Installation + +To install MaksIT.Core, add the package to your project via NuGet: + +```sh +dotnet add package MaksIT.Core +``` + +Or manually add it to your `.csproj` file: + +```xml + +``` + +## Usage + +### Enumeration + +The `Enumeration` base class provides a way to create strongly-typed enums in C#. This is useful for scenarios where you need more functionality than the default `enum` type. + +**Example:** + +```csharp +public class Status : Enumeration +{ + public static readonly Status Active = new Status(1, "Active"); + public static readonly Status Inactive = new Status(2, "Inactive"); + + private Status(int id, string name) : base(id, name) { } +} + +// Usage +var activeStatus = Status.FromValue(1); +Console.WriteLine(activeStatus.Name); // Output: Active +``` + +### Guid Extensions + +The `GuidExtensions` class contains extensions for working with `Guid` types. + +**Example:** + +```csharp +Guid guid = Guid.NewGuid(); +Guid? nullableGuid = guid.ToNullable(); +``` + +### Object Extensions + +The `ObjectExtensions` class provides extensions for working with objects. + +**Example:** + +```csharp +var person = new { Name = "John", Age = 30 }; +string json = person.ToJson(); +Console.WriteLine(json); // Output: {"name":"John","age":30} +``` + +### String Extensions + +The `StringExtensions` class provides a variety of useful string manipulation methods. + +**Example:** + +```csharp +string text = "Hello World"; +bool isLike = text.Like("Hello*"); // SQL-like matching +Console.WriteLine(isLike); // Output: True +``` + +## Available Methods + +### Enumeration Methods + +- **`GetAll()`**: Retrieves all static fields of a given type `T` that derive from `Enumeration`. +- **`Equals(object? obj)`**: Determines whether the specified object is equal to the current object. +- **`GetHashCode()`**: Returns the hash code for the current object. +- **`AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)`**: Computes the absolute difference between two enumeration values. +- **`FromValue(int value)`**: Retrieves an instance of type `T` from its integer value. +- **`FromDisplayName(string displayName)`**: Retrieves an instance of type `T` from its display name. +- **`CompareTo(object? other)`**: Compares the current instance with another object of the same type. + +### Guid Methods + +- **`ToNullable(this Guid id)`**: Converts a `Guid` to a nullable `Guid?`. Returns `null` if the `Guid` is `Guid.Empty`. + +### Object Methods + +- **`ToJson(this T? obj)`**: Converts an object to a JSON string using default serialization options. +- **`ToJson(this T? obj, List? converters)`**: Converts an object to a JSON string using custom converters. + +### String Methods + +- **`Like(this string? text, string? wildcardedText)`**: Determines if a string matches a given wildcard pattern (SQL LIKE). +- **`Left(this string s, int count)`**: Returns the left substring of the specified length. +- **`Right(this string s, int count)`**: Returns the right substring of the specified length. +- **`Mid(this string s, int index, int count)`**: Returns a substring starting from the specified index with the specified length. +- **`ToInteger(this string s)`**: Converts a string to an integer, returning zero if conversion fails. +- **`IsInteger(this string s)`**: Determines whether the string represents an integer. +- **`Prepend(this StringBuilder sb, string content)`**: Prepends content to the beginning of a `StringBuilder`. +- **`ToEnum(this string input)`**: Converts a string to an enum value of type `T`. +- **`ToNullableEnum(this string input)`**: Converts a string to a nullable enum value of type `T`. +- **`ToNull(this string s)`**: Returns `null` if the string is empty or whitespace. +- **`NullIfEmptyString(this string s)`**: Returns `null` if the string is empty or whitespace, otherwise returns the original string. +- **`ToLong(this string s)`**: Converts a string to a long, returning a hash code if conversion fails. +- **`ToNullableLong(this string s)`**: Converts a string to a nullable long, returning `null` if conversion fails. +- **`ToInt(this string s)`**: Converts a string to an int, returning a hash code if conversion fails. +- **`ToNullableInt(this string s)`**: Converts a string to a nullable int, returning `null` if conversion fails. +- **`ToUint(this string s)`**: Converts a string to a uint, returning a hash code if conversion fails. +- **`ToNullableUint(this string s)`**: Converts a string to a nullable uint, returning `null` if conversion fails. +- **`ToDecimal(this string s)`**: Converts a string to a decimal, returning a hash code if conversion fails. +- **`ToNullableDecimal(this string s)`**: Converts a string to a nullable decimal, returning `null` if conversion fails. +- **`ToDouble(this string s)`**: Converts a string to a double, returning a hash code if conversion fails. +- **`ToNullableDouble(this string s)`**: Converts a string to a nullable double, returning `null` if conversion fails. +- **`ToDate(this string s, string[] formats)`**: Converts a string to a `DateTime` object using a specified format. +- **`ToDate(this string s)`**: Converts a string to a `DateTime` object using the default format. +- **`ToNullableDate(this string s)`**: Converts a string to a nullable `DateTime` object using the default format. +- **`ToNullableDate(this string s, string[] formats)`**: Converts a string to a nullable `DateTime` object using specified formats. +- **`ToDateTime(this string s, string[] formats)`**: Converts a string to a `DateTime` object using specified formats. +- **`ToDateTime(this string s)`**: Converts a string to a `DateTime` object using the default formats. +- **`ToNullableDateTime(this string s)`**: Converts a string to a nullable `DateTime` object using the default formats. +- **`ToNullableDateTime(this string s, string[] formats)`**: Converts a string to a nullable `DateTime` object using specified formats. +- **`ToBool(this string s)`**: Converts a string to a boolean. +- **`ToNullableBool(this string s)`**: Converts a string to a nullable boolean. +- **`ToGuid(this string text)`**: Converts a string to a `Guid`. +- **`ToNullableGuid(this string s)`**: Converts a string to a nullable `Guid`. +- **`StringSplit(this string s, char c)`**: Splits a string by a specified character and trims each resulting element. +- **`ToTitle(this string s)`**: Converts the first character of the string to uppercase. +- **`ExtractUrls(this string s)`**: Extracts all URLs from a string. +- **`Format(this string s, params object[] args)`**: Formats a string using specified arguments. +- **`Excerpt(this string s, int length = 60)`**: Truncates a string to a specified length, adding ellipses if necessary. +- **`ToObject(this string s)`**: Deserializes a JSON string into an object of type `T`. +- **`ToObject(this string s, List converters)`**: Deserializes a JSON string into an object of type `T` using custom converters. +- **`IsValidEmail(this string? s)`**: Validates whether the string is a valid email format. +- **`HtmlToPlainText(this string htmlCode)`**: Converts HTML content to plain text. +- **`ToCamelCase(this string input)`**: Converts a string to camel case. + +## Contribution + +Contributions to this project are welcome! Please fork the repository and submit a pull request with your changes. If you encounter any issues or have feature requests, feel free to open an issue on GitHub. + +## License + +This project is licensed under the MIT License. See the full license text below. + +--- + +### MIT License + +``` +MIT License + +Copyright (c) 2024 Maksym Sadovnychyy (MAKS-IT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Contact + +For any questions or inquiries, please reach out via GitHub or [email](mailto:maksym.sadovnychyy@gmail.com). \ No newline at end of file diff --git a/src/MaksIT.Core.Tests/Abstractions/EnumerationTests.cs b/src/MaksIT.Core.Tests/Abstractions/EnumerationTests.cs new file mode 100644 index 0000000..8356269 --- /dev/null +++ b/src/MaksIT.Core.Tests/Abstractions/EnumerationTests.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace MaksIT.Core.Abstractions.Tests { + + public class TestEnumeration : Enumeration { + public static readonly TestEnumeration First = new TestEnumeration(1, "First"); + public static readonly TestEnumeration Second = new TestEnumeration(2, "Second"); + public static readonly TestEnumeration Third = new TestEnumeration(3, "Third"); + + public TestEnumeration(int id, string name) : base(id, name) { } + } + + + public class EnumerationTests { + [Fact] + public void GetAll_ShouldReturnAllEnumerations() { + // Act + var allValues = Enumeration.GetAll().ToList(); + + // Assert + Assert.NotNull(allValues); + Assert.Equal(3, allValues.Count); + Assert.Contains(TestEnumeration.First, allValues); + Assert.Contains(TestEnumeration.Second, allValues); + Assert.Contains(TestEnumeration.Third, allValues); + } + + [Theory] + [InlineData(1, "First")] + [InlineData(2, "Second")] + [InlineData(3, "Third")] + public void FromValue_ShouldReturnEnumerationByValue(int id, string expectedName) { + // Act + var result = Enumeration.FromValue(id); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedName, result.Name); + } + + [Theory] + [InlineData("First", 1)] + [InlineData("Second", 2)] + [InlineData("Third", 3)] + public void FromDisplayName_ShouldReturnEnumerationByName(string displayName, int expectedId) { + // Act + var result = Enumeration.FromDisplayName(displayName); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + } + + [Fact] + public void AbsoluteDifference_ShouldReturnCorrectDifference() { + // Act + var difference = Enumeration.AbsoluteDifference(TestEnumeration.First, TestEnumeration.Third); + + // Assert + Assert.Equal(2, difference); + } + + [Fact] + public void Equals_SameReference_ShouldReturnTrue() { + // Act + var result = TestEnumeration.First.Equals(TestEnumeration.First); + + // Assert + Assert.True(result); + } + + [Fact] + public void Equals_DifferentReferencesSameValues_ShouldReturnTrue() { + // Arrange + var firstCopy = Enumeration.FromValue(1); + + // Act + var result = TestEnumeration.First.Equals(firstCopy); + + // Assert + Assert.True(result); + } + + [Fact] + public void Equals_DifferentValues_ShouldReturnFalse() { + // Act + var result = TestEnumeration.First.Equals(TestEnumeration.Second); + + // Assert + Assert.False(result); + } + + [Fact] + public void CompareTo_ShouldReturnZeroForEqualValues() { + // Arrange + var firstCopy = Enumeration.FromValue(1); + + // Act + var result = TestEnumeration.First.CompareTo(firstCopy); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void CompareTo_ShouldReturnPositiveForGreaterValue() { + // Act + var result = TestEnumeration.Second.CompareTo(TestEnumeration.First); + + // Assert + Assert.True(result > 0); + } + + [Fact] + public void CompareTo_ShouldReturnNegativeForLesserValue() { + // Act + var result = TestEnumeration.First.CompareTo(TestEnumeration.Second); + + // Assert + Assert.True(result < 0); + } + + [Fact] + public void CompareTo_InvalidComparison_ShouldThrowArgumentException() { + // Arrange + var nonEnumerationObject = new object(); + + // Act & Assert + Assert.Throws(() => TestEnumeration.First.CompareTo(nonEnumerationObject)); + } + + [Fact] + public void GetHashCode_ShouldReturnIdHashCode() { + // Act + var hashCode = TestEnumeration.First.GetHashCode(); + + // Assert + Assert.Equal(TestEnumeration.First.Id.GetHashCode(), hashCode); + } + + [Fact] + public void ToString_ShouldReturnName() { + // Act + var result = TestEnumeration.First.ToString(); + + // Assert + Assert.Equal("First", result); + } + + [Fact] + public void Parse_InvalidValue_ShouldThrowInvalidOperationException() { + // Act & Assert + Assert.Throws(() => Enumeration.FromValue(999)); + } + + [Fact] + public void Parse_InvalidDisplayName_ShouldThrowInvalidOperationException() { + // Act & Assert + Assert.Throws(() => Enumeration.FromDisplayName("NonExistent")); + } + } +} diff --git a/src/MaksIT.Core.Tests/Extensions/GuidExtensionsTests.cs b/src/MaksIT.Core.Tests/Extensions/GuidExtensionsTests.cs new file mode 100644 index 0000000..47d9e95 --- /dev/null +++ b/src/MaksIT.Core.Tests/Extensions/GuidExtensionsTests.cs @@ -0,0 +1,72 @@ +using System; +using Xunit; + +namespace MaksIT.Core.Extensions.Tests { + public class GuidExtensionsTests { + [Fact] + public void ToNullable_WithEmptyGuid_ShouldReturnNull() { + // Arrange + var emptyGuid = Guid.Empty; + + // Act + var result = emptyGuid.ToNullable(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToNullable_WithNonEmptyGuid_ShouldReturnSameGuid() { + // Arrange + var nonEmptyGuid = Guid.NewGuid(); + + // Act + var result = nonEmptyGuid.ToNullable(); + + // Assert + Assert.NotNull(result); + Assert.Equal(nonEmptyGuid, result); + } + + [Fact] + public void ToNullable_WithDefaultGuid_ShouldReturnNull() { + // Arrange + var defaultGuid = default(Guid); + + // Act + var result = defaultGuid.ToNullable(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToNullable_WithSameGuidTwice_ShouldReturnSameGuidEachTime() { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var result1 = guid.ToNullable(); + var result2 = guid.ToNullable(); + + // Assert + Assert.Equal(result1, result2); + } + + [Fact] + public void ToNullable_WithMultipleNewGuids_ShouldReturnUniqueNonEmptyResults() { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + // Act + var result1 = guid1.ToNullable(); + var result2 = guid2.ToNullable(); + + // Assert + Assert.NotEqual(result1, result2); + Assert.NotNull(result1); + Assert.NotNull(result2); + } + } +} diff --git a/src/MaksIT.Core.Tests/Extensions/ObjectExtensionsTests.cs b/src/MaksIT.Core.Tests/Extensions/ObjectExtensionsTests.cs new file mode 100644 index 0000000..4f21a29 --- /dev/null +++ b/src/MaksIT.Core.Tests/Extensions/ObjectExtensionsTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using MaksIT.Core.Extensions; +using Xunit; + +namespace MaksIT.Core.Tests.Extensions { + public class ObjectExtensionsTests { + private class TestObject { + public required string Name { get; set; } + public int Age { get; set; } + public string? Address { get; set; } + } + + private class CustomDateTimeConverter : JsonConverter { + private readonly string _format; + + public CustomDateTimeConverter(string format) { + _format = format; + } + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var dateString = reader.GetString(); + if (dateString is null) { + throw new JsonException("Expected a date string but got null."); + } + + return DateTime.ParseExact(dateString, _format, null); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { + writer.WriteStringValue(value.ToString(_format)); + } + } + + [Fact] + public void ToJson_WithNullObject_ShouldReturnEmptyJson() { + // Arrange + TestObject? obj = null; + + // Act + var result = obj.ToJson(); + + // Assert + Assert.Equal("{}", result); + } + + [Fact] + public void ToJson_WithSimpleObject_ShouldReturnCorrectJson() { + // Arrange + var obj = new TestObject { + Name = "John Doe", + Age = 30 + }; + + // Act + var result = obj.ToJson(); + + // Assert + Assert.Equal("{\"name\":\"John Doe\",\"age\":30}", result); + } + + [Fact] + public void ToJson_WithObjectAndNullValues_ShouldIgnoreNullProperties() { + // Arrange + var obj = new TestObject { + Name = "John Doe", + Age = 30, + Address = null + }; + + // Act + var result = obj.ToJson(); + + // Assert + Assert.Equal("{\"name\":\"John Doe\",\"age\":30}", result); + } + + [Fact] + public void ToJson_WithCustomJsonConverter_ShouldApplyConverter() { + // Arrange + var obj = new DateTime(2023, 08, 30); + var converters = new List { new CustomDateTimeConverter("yyyy-MM-dd") }; + + // Act + var result = obj.ToJson(converters); + + // Assert + Assert.Equal("\"2023-08-30\"", result); + } + + [Fact] + public void ToJson_WithComplexObjectAndConverters_ShouldSerializeCorrectly() { + // Arrange + var obj = new { + Name = "Jane Doe", + BirthDate = new DateTime(1990, 12, 25) + }; + + var converters = new List { new CustomDateTimeConverter("yyyy/MM/dd") }; + + // Act + var result = obj.ToJson(converters); + + // Assert + Assert.Equal("{\"name\":\"Jane Doe\",\"birthDate\":\"1990/12/25\"}", result); + } + + [Fact] + public void ToJson_WithEmptyObject_ShouldReturnEmptyJsonObject() { + // Arrange + var obj = new { }; + + // Act + var result = obj.ToJson(); + + // Assert + Assert.Equal("{}", result); + } + } +} diff --git a/src/MaksIT.Core.Tests/Extensions/StringExtensionsTests.cs b/src/MaksIT.Core.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 0000000..8afb0bb --- /dev/null +++ b/src/MaksIT.Core.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using MaksIT.Core.Extensions; +using Xunit; + +namespace MaksIT.Core.Tests.Extensions { + public class StringExtensionsTests { + [Theory] + [InlineData("Hello World", "H*", true)] // Match starts with 'H' + [InlineData("Hello World", "h*", true)] // Case insensitive match + [InlineData("Hello World", "*World", true)] // Match ends with 'World' + [InlineData("Hello World", "Hello?World", true)] // '?' should match exactly one character (space in this case) + [InlineData("Hello World", "*W?rld", true)] // '?' matches 'o' in 'World' + [InlineData("Hello World", "Goodbye*", false)] // No match for 'Goodbye*' + public void Like_ShouldReturnExpectedResults(string input, string pattern, bool expected) { + // Act + bool result = input.Like(pattern); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("abcdef", 3, "abc")] + [InlineData("abcdef", 0, "")] + [InlineData("abcdef", 10, "abcdef")] + public void Left_ShouldReturnLeftSubstring(string input, int count, string expected) { + // Act + string result = input.Left(count); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("abcdef", 3, "def")] + [InlineData("abcdef", 0, "")] + [InlineData("abcdef", 10, "abcdef")] + public void Right_ShouldReturnRightSubstring(string input, int count, string expected) { + // Act + string result = input.Right(count); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("abcdef", 1, 3, "bcd")] + [InlineData("abcdef", 0, 2, "ab")] + [InlineData("abcdef", 4, 10, "ef")] + [InlineData("abcdef", 6, 2, "")] + public void Mid_ShouldReturnSubstring(string input, int index, int count, string expected) { + // Act + string result = input.Mid(index, count); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("123", 123)] + [InlineData("abc", 0)] + [InlineData(null, 0)] + [InlineData("", 0)] + public void ToInteger_ShouldConvertToInteger(string input, int expected) { + // Act + int result = input.ToInteger(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("123", true)] + [InlineData("-123", true)] + [InlineData("abc", false)] + [InlineData("123abc", false)] + public void IsInteger_ShouldReturnIfStringIsInteger(string input, bool expected) { + // Act + bool result = input.IsInteger(); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void Prepend_ShouldPrependStringToStringBuilder() { + // Arrange + var sb = new StringBuilder("World"); + string content = "Hello "; + + // Act + sb.Prepend(content); + + // Assert + Assert.Equal("Hello World", sb.ToString()); + } + + [Theory] + [InlineData("1", DayOfWeek.Monday)] + [InlineData("Tuesday", DayOfWeek.Tuesday)] + [InlineData("5", DayOfWeek.Friday)] + public void ToEnum_ShouldConvertStringToEnum(string input, DayOfWeek expected) { + // Act + DayOfWeek result = input.ToEnum(); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void ToEnum_InvalidValue_ShouldThrowNotSupportedException() { + // Arrange + string input = "NotAnEnumValue"; + + // Act & Assert + Assert.Throws(() => input.ToEnum()); + } + + [Theory] + [InlineData(" ", null)] + [InlineData("", null)] + [InlineData("valid", "valid")] + public void ToNull_ShouldReturnNullForWhitespaceOrEmptyString(string input, string expected) { + // Act + var result = input.ToNull(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("123", 123L)] + public void ToLong_ShouldConvertToLong_WhenValidLong(string input, long? expected) { + // Act + var result = input.ToLong(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("abc", null)] + [InlineData("", null)] + public void ToLong_ShouldReturnNull_WhenInvalidLong(string input, long? expected) { + // Act + var result = input.ToLong(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("2021-08-30", new[] { "yyyy-MM-dd" }, "2021-08-30T00:00:00Z")] + [InlineData("30/08/2021", new[] { "dd/MM/yyyy" }, "2021-08-30T00:00:00Z")] + public void ToDate_ShouldConvertToDate(string input, string[] formats, string expected) { + // Act + var result = input.ToDate(formats); + + // Assert + Assert.Equal(DateTime.Parse(expected, null, DateTimeStyles.RoundtripKind), result); + } + + [Theory] + [InlineData("2021-08-30T00:00:00Z", "2021-08-30T00:00:00Z")] + [InlineData("Now", "Now")] + public void ToDateTime_ShouldConvertToDateTime(string input, string expected) { + // Act + var result = input.ToDateTime(); + + // Assert + if (expected == "Now") { + Assert.Equal(DateTime.Now.ToString("dd/MM/yyyy"), result.ToString("dd/MM/yyyy")); + } + else { + Assert.Equal(DateTime.Parse(expected, null, DateTimeStyles.RoundtripKind), result); + } + } + + [Theory] + [InlineData("ok", true)] + [InlineData("yes", true)] + [InlineData("true", true)] + [InlineData("1", true)] + [InlineData("no", false)] + [InlineData("false", false)] + [InlineData("0", false)] + [InlineData("invalid", false)] + public void ToBool_ShouldConvertToBool(string input, bool expected) { + // Act + var result = input.ToBool(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("e02fd0e4-00fd-090A-ca30-0d00a0038ba0", "e02fd0e4-00fd-090a-ca30-0d00a0038ba0")] + [InlineData("invalid-guid", null)] + public void ToGuid_ShouldConvertStringToGuid(string input, string expected) { + // Act + if (expected == null) { + var result = input.ToGuid(); + + // Assert that it returns a valid Guid (from MD5 hash, not an exception) + Assert.IsType(result); + Assert.NotEqual(Guid.Empty, result); // Check that it does not return Guid.Empty + } + else { + var result = input.ToGuid(); + + // Assert + Assert.Equal(Guid.Parse(expected), result); + } + } + + [Theory] + [InlineData("

Hello World

", "Hello World")] + [InlineData("

Hello

", "Hello")] + public void HtmlToPlainText_ShouldConvertHtmlToPlainText(string input, string expected) { + // Act + var result = input.HtmlToPlainText(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("hello world", "helloWorld")] + [InlineData("Hello World", "helloWorld")] + [InlineData("HELLO_WORLD", "helloWorld")] + [InlineData("HELLO-WORLD", "helloWorld")] + public void ToCamelCase_ShouldConvertToCamelCase(string input, string expected) { + // Act + var result = input.ToCamelCase(); + + // Assert + Assert.Equal(expected, result); + } + } +} diff --git a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj new file mode 100644 index 0000000..2833ca3 --- /dev/null +++ b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/src/MaksIT.Core.sln b/src/MaksIT.Core.sln new file mode 100644 index 0000000..d10e2d8 --- /dev/null +++ b/src/MaksIT.Core.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.Core", "MaksIT.Core\MaksIT.Core.csproj", "{4AE39520-D4F7-4C5F-ACE9-9E79AEAF3228}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaksIT.Core.Tests", "MaksIT.Core.Tests\MaksIT.Core.Tests.csproj", "{B67A43DA-AFFC-4510-8D51-08F1FF84CC5B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4AE39520-D4F7-4C5F-ACE9-9E79AEAF3228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AE39520-D4F7-4C5F-ACE9-9E79AEAF3228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AE39520-D4F7-4C5F-ACE9-9E79AEAF3228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AE39520-D4F7-4C5F-ACE9-9E79AEAF3228}.Release|Any CPU.Build.0 = Release|Any CPU + {B67A43DA-AFFC-4510-8D51-08F1FF84CC5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B67A43DA-AFFC-4510-8D51-08F1FF84CC5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B67A43DA-AFFC-4510-8D51-08F1FF84CC5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B67A43DA-AFFC-4510-8D51-08F1FF84CC5B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9BCC72D1-8BE8-4924-AF73-C8E86E16EC59} + EndGlobalSection +EndGlobal diff --git a/src/MaksIT.Core/Abstractions/Enumeration.cs b/src/MaksIT.Core/Abstractions/Enumeration.cs new file mode 100644 index 0000000..47efbb3 --- /dev/null +++ b/src/MaksIT.Core/Abstractions/Enumeration.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MaksIT.Core.Abstractions { + public abstract class Enumeration : IComparable { + public string Name { get; } + + public int Id { get; } + + protected Enumeration(int id, string name) => (Id, Name) = (id, name); + + public override string ToString() => Name; + + public static IEnumerable GetAll() where T : Enumeration => + typeof(T).GetFields(BindingFlags.Public | + BindingFlags.Static | + BindingFlags.DeclaredOnly) + .Select(f => f.GetValue(null)) + .Cast(); + + public override bool Equals(object? obj) => + obj is Enumeration otherValue && + GetType() == obj.GetType() && + Id == otherValue.Id; + + public override int GetHashCode() => Id.GetHashCode(); + + public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) => + Math.Abs(firstValue.Id - secondValue.Id); + + public static T FromValue(int value) where T : Enumeration => + Parse(value, nameof(value), item => item.Id == value); + + public static T FromDisplayName(string displayName) where T : Enumeration => + Parse(displayName, nameof(displayName), item => item.Name == displayName); + + private static T Parse(TK value, string description, Func predicate) where T : Enumeration => + GetAll().FirstOrDefault(predicate) ?? + throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}"); + + public int CompareTo(object? other) { + if (other is Enumeration otherEnumeration) + return Id.CompareTo(otherEnumeration.Id); + else + throw new ArgumentException($"Object is not of type {nameof(Enumeration)}"); + } + } +} diff --git a/src/MaksIT.Core/Extensions/GuidExtensions.cs b/src/MaksIT.Core/Extensions/GuidExtensions.cs new file mode 100644 index 0000000..f4d1ee5 --- /dev/null +++ b/src/MaksIT.Core/Extensions/GuidExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.Core.Extensions { + public static class GuidExtensions { + public static Guid? ToNullable(this Guid id) { + // Return null if the Guid is the default value (Guid.Empty) + return id == default ? null : id; + } + } + +} diff --git a/src/MaksIT.Core/Extensions/ObjectExtensions.cs b/src/MaksIT.Core/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..88167ed --- /dev/null +++ b/src/MaksIT.Core/Extensions/ObjectExtensions.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MaksIT.Core.Extensions; + +public static class ObjectExtensions { + + /// + /// Converts object to json string + /// + /// + /// + /// + public static string ToJson(this T? obj) => obj.ToJson(null); + + /// + /// Converts object to json string + /// + /// + /// + /// + /// + public static string ToJson(this T? obj, List? converters) { + if (obj == null) + return "{}"; + + var options = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + converters?.ForEach(x => options.Converters.Add(x)); + + return JsonSerializer.Serialize(obj, options); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Extensions/StringExtensions.cs b/src/MaksIT.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..6cf853f --- /dev/null +++ b/src/MaksIT.Core/Extensions/StringExtensions.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace MaksIT.Core.Extensions { + public static partial class StringExtensions { + /// + /// SQL Like implementation using wildcard patterns. + /// + public static bool Like(this string? text, string? wildcardedText) { + if (text is null || wildcardedText is null) return false; + + return Regex.IsMatch(text, wildcardedText.WildcardToRegular(), RegexOptions.IgnoreCase | RegexOptions.Multiline); + } + + /// + /// Converts a wildcarded string to a regular expression. + /// + private static string WildcardToRegular(this string value) => + $"^{Regex.Escape(value).Replace("\\?", ".").Replace("\\*", ".*")}$"; + + /// + /// Returns the left substring of the specified length. + /// + public static string Left(this string s, int count) => + s.Substring(0, Math.Min(count, s.Length)); + + /// + /// Returns the right substring of the specified length. + /// + public static string Right(this string s, int count) => + s.Substring(Math.Max(0, s.Length - count)); + + /// + /// Returns a substring starting from the specified index with the specified length. + /// + public static string Mid(this string s, int index, int count) => + s.Substring(index, Math.Min(count, s.Length - index)); + + /// + /// Converts the string to an integer, returning zero if conversion fails. + /// + public static int ToInteger(this string s) => + int.TryParse(s, out var integerValue) ? integerValue : 0; + + /// + /// Determines whether the string represents an integer. + /// + public static bool IsInteger(this string s) => Regex.IsMatch(s, @"^-?\d+$"); + + public static StringBuilder Prepend(this StringBuilder sb, string content) => sb.Insert(0, content); + + public static T ToEnum(this string input) where T : struct { + if (string.IsNullOrWhiteSpace(input)) + throw new ArgumentException("Input cannot be null or empty.", nameof(input)); + + if (Enum.TryParse(input, true, out T result)) + return result; + + var enumType = typeof(T); + + foreach (T enumItem in Enum.GetValues(enumType)) { + var att = enumType.GetMember(enumItem.ToString() ?? string.Empty)[0] + .GetCustomAttributes(typeof(DisplayAttribute), false) + .SingleOrDefault() as DisplayAttribute; + + var displayName = att?.GetName(); + + if (input.Equals(displayName, StringComparison.InvariantCultureIgnoreCase)) + return enumItem; + } + + throw new NotSupportedException($"Cannot parse the value '{input}' for {enumType}"); + } + + public static T? ToNullableEnum(this string input) where T : struct => + !string.IsNullOrWhiteSpace(input) ? input.ToEnum() : null; + + public static string? ToNull(this string s) => string.IsNullOrWhiteSpace(s) ? null : s; + + public static string? NullIfEmptyString(this string s) => s.ToNull(); + + public static long? ToLong(this string s) => + long.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result) ? result : (long?)null; + + public static long? ToNullableLong(this string s) => string.IsNullOrWhiteSpace(s) ? (long?)null : s.ToLong(); + + public static int? ToInt(this string s) => + int.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result) ? result : (int?)null; + + public static int? ToNullableInt(this string s) => string.IsNullOrWhiteSpace(s) ? (int?)null : s.ToInt(); + + public static uint? ToUint(this string s) => + uint.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result) ? result : (uint?)null; + + public static uint? ToNullableUint(this string s) => string.IsNullOrWhiteSpace(s) ? (uint?)null : s.ToUint(); + + public static decimal? ToDecimal(this string s) => + decimal.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result) ? result : (decimal?)null; + + public static decimal? ToNullableDecimal(this string s) => string.IsNullOrWhiteSpace(s) ? (decimal?)null : s.ToDecimal(); + + public static double? ToDouble(this string s) => + double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result) ? result : (double?)null; + + public static double? ToNullableDouble(this string s) => string.IsNullOrWhiteSpace(s) ? (double?)null : s.ToDouble(); + + #region DateTime + public static DateTime ToDate(this string s, string[] formats) => + DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var datetime) + ? DateTime.SpecifyKind(datetime, DateTimeKind.Utc) + : throw new FormatException($"The date [{s}] is not in the required format: [{formats[0]}]"); + + public static DateTime ToDate(this string s) => s.ToDate(new[] { "dd/MM/yyyy" }); + + public static DateTime? ToNullableDate(this string s) => string.IsNullOrEmpty(s) ? (DateTime?)null : s.ToDate(); + + public static DateTime? ToNullableDate(this string s, string[] formats) => string.IsNullOrEmpty(s) ? (DateTime?)null : s.ToDate(formats); + + public static DateTime ToDateTime(this string s, string[] formats) { + if (s.Equals("Now", StringComparison.OrdinalIgnoreCase)) return DateTime.Now; + if (s.Equals("UtcNow", StringComparison.OrdinalIgnoreCase)) return DateTime.UtcNow; + if (s.Equals("Today", StringComparison.OrdinalIgnoreCase)) return DateTime.Today; + + return DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) + ? DateTime.SpecifyKind(result, DateTimeKind.Utc) + : throw new FormatException($"Unable to parse exact date from value [{s}] with formats [{string.Join(", ", formats)}]"); + } + + public static DateTime ToDateTime(this string s) => s.ToDateTime(new[] { "dd/MM/yyyy", "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy HH:mm", "yyyy-MM-dd'T'HH:mm:ss'Z'" }); + + public static DateTime? ToNullableDateTime(this string s) => string.IsNullOrWhiteSpace(s) ? (DateTime?)null : s.ToDateTime(); + + public static DateTime? ToNullableDateTime(this string s, string[] formats) => string.IsNullOrWhiteSpace(s) ? (DateTime?)null : s.ToDateTime(formats); + #endregion + + public static bool ToBool(this string s) => + new[] { "ok", "yes", "y", "true", "1" }.Contains(s, StringComparer.InvariantCultureIgnoreCase); + + public static bool? ToNullableBool(this string s) => string.IsNullOrWhiteSpace(s) ? (bool?)null : s.ToBool(); + + public static Guid ToGuid(this string text) => + Guid.TryParse(text, out var value) ? value : new Guid(MD5.Create().ComputeHash(Encoding.Default.GetBytes(text.ToUpper()))); + + public static Guid? ToNullableGuid(this string s) => string.IsNullOrWhiteSpace(s) ? (Guid?)null : s.ToGuid(); + + public static string[] StringSplit(this string s, char c) => + s.Split(c).Select(x => x.Trim()).ToArray(); + + public static string ToTitle(this string s) => string.IsNullOrWhiteSpace(s) ? s : char.ToUpper(s[0]) + s[1..]; + + [GeneratedRegex(@"(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])", RegexOptions.Compiled)] + private static partial Regex UrlsRegex(); + + public static IEnumerable ExtractUrls(this string s) => + UrlsRegex().Matches(s).Cast() + .Select(match => match.Value) + .Where(url => Uri.TryCreate(url, UriKind.Absolute, out _)) + .Select(url => new Uri(url)) + .Distinct(); + + public static string Format(this string s, params object[] args) => string.Format(s, args); + + public static string Excerpt(this string s, int length = 60) => + string.IsNullOrWhiteSpace(s) ? s : s.Length <= length ? s : $"{s.Substring(0, length - 3)}..."; + + public static T? ToObject(this string s) => ToObjectCore(s, null); + + public static T? ToObject(this string s, List converters) => ToObjectCore(s, converters); + + private static T? ToObjectCore(string s, List? converters) { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + converters?.ForEach(x => options.Converters.Add(x)); + return JsonSerializer.Deserialize(s, options); + } + + public static bool IsValidEmail(this string? s) { + if (s is null) return false; + + const string pattern = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$"; + var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(2)); + + return regex.IsMatch(s); + } + + public static string HtmlToPlainText(this string htmlCode) { + if (string.IsNullOrEmpty(htmlCode)) + return htmlCode; + + var sb = new StringBuilder(htmlCode); + + // Remove new lines, tabs, and multiple spaces + sb.Replace("\n", " ").Replace("\t", " "); + sb = new StringBuilder(Regex.Replace(sb.ToString(), "\\s+", " ")); + + // Remove and ", "", RegexOptions.IgnoreCase | RegexOptions.Singleline)); + + // Replace HTML entities + string[] oldWords = { " ", "&", """, "<", ">", "®", "©", "•", "™", "'" }; + string[] newWords = { " ", "&", "\"", "<", ">", "®", "©", "•", "™", "'" }; + for (int i = 0; i < oldWords.Length; i++) sb.Replace(oldWords[i], newWords[i]); + + // Handle line breaks + sb.Replace("
", "\n").Replace("
]*>", "").Trim(); + + return plainText; + } + + public static string ToCamelCase(this string input) { + if (string.IsNullOrEmpty(input)) return input; + + var words = input.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < words.Length; i++) { + words[i] = i == 0 ? words[i].ToLower() : char.ToUpper(words[i][0]) + words[i][1..].ToLower(); + } + + return string.Join("", words); + } + } +} diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj new file mode 100644 index 0000000..7c5a13c --- /dev/null +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + MaksIT.$(MSBuildProjectName.Replace(" ", "_")) + + + MaksIT.Core + 1.0.0 + Maksym Sadovnychyy + MAKS-IT + MaksIT.Core + MaksIT.Core is a collection of helper methods and extensions for .NET projects, designed to simplify common tasks and improve code readability. The library includes extensions for `Guid`, `string`, `Object`, and a base class for creating enumeration types. + dotnet;enumeration;string;guid;object;parsers;extensions + https://github.com/MAKS-IT-COM/maksit-core + MIT + false + README.md + + + + + + diff --git a/src/Release-NuGetPackage.bat b/src/Release-NuGetPackage.bat new file mode 100644 index 0000000..ba9cefe --- /dev/null +++ b/src/Release-NuGetPackage.bat @@ -0,0 +1,7 @@ +@echo off + +REM Change directory to the location of the script +cd /d %~dp0 + +REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory +powershell -ExecutionPolicy Bypass -File "%~dp0Release-NuGetPackage.ps1" diff --git a/src/Release-NuGetPackage.ps1 b/src/Release-NuGetPackage.ps1 new file mode 100644 index 0000000..8e69465 --- /dev/null +++ b/src/Release-NuGetPackage.ps1 @@ -0,0 +1,46 @@ +# Retrieve the API key from the environment variable +$apiKey = $env:NUGET_MAKS_IT +if (-not $apiKey) { + Write-Host "Error: API key not found in environment variable NUGET_MAKS_IT." + exit 1 +} + +# NuGet source +$nugetSource = "https://api.nuget.org/v3/index.json" + +# Define paths +$solutionDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectDir = "$solutionDir\MaksIT.Core" +$outputDir = "$projectDir\bin\Release" + +# Clean previous builds +Write-Host "Cleaning previous builds..." +dotnet clean $projectDir -c Release + +# Build the project +Write-Host "Building the project..." +dotnet build $projectDir -c Release + +# Pack the NuGet package +Write-Host "Packing the project..." +dotnet pack $projectDir -c Release --no-build + +# Look for the .nupkg file +$packageFile = Get-ChildItem -Path $outputDir -Filter "*.nupkg" -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + +if ($packageFile) { + Write-Host "Package created successfully: $($packageFile.FullName)" + + # Push the package to NuGet + Write-Host "Pushing the package to NuGet..." + dotnet nuget push $packageFile.FullName -k $apiKey -s $nugetSource --skip-duplicate + + if ($LASTEXITCODE -eq 0) { + Write-Host "Package pushed successfully." + } else { + Write-Host "Failed to push the package." + } +} else { + Write-Host "Package creation failed. No .nupkg file found." + exit 1 +} diff --git a/src/Release-NuGetPackage.sh b/src/Release-NuGetPackage.sh new file mode 100644 index 0000000..25e6691 --- /dev/null +++ b/src/Release-NuGetPackage.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Retrieve the API key from the environment variable +apiKey=$NUGET_MAKS_IT +if [ -z "$apiKey" ]; then + echo "Error: API key not found in environment variable NUGET_MAKS_IT." + exit 1 +fi + +# NuGet source +nugetSource="https://api.nuget.org/v3/index.json" + +# Define paths +scriptDir=$(dirname "$0") +solutionDir=$(realpath "$scriptDir") +projectDir="$solutionDir/MaksIT.Core" +outputDir="$projectDir/bin/Release" + +# Clean previous builds +echo "Cleaning previous builds..." +dotnet clean "$projectDir" -c Release + +# Build the project +echo "Building the project..." +dotnet build "$projectDir" -c Release + +# Pack the NuGet package +echo "Packing the project..." +dotnet pack "$projectDir" -c Release --no-build + +# Look for the .nupkg file +packageFile=$(find "$outputDir" -name "*.nupkg" -print0 | xargs -0 ls -t | head -n 1) + +if [ -n "$packageFile" ]; then + echo "Package created successfully: $packageFile" + + # Push the package to NuGet + echo "Pushing the package to NuGet..." + dotnet nuget push "$packageFile" -k "$apiKey" -s "$nugetSource" --skip-duplicate + + if [ $? -eq 0 ]; then + echo "Package pushed successfully." + else + echo "Failed to push the package." + fi +else + echo "Package creation failed. No .nupkg file found." + exit 1 +fi