diff --git a/.editorconfig b/.editorconfig index 30024f0..5f33bb7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -85,6 +85,7 @@ dotnet_diagnostic.IDE0301.severity = none # simplify collection initialization dotnet_diagnostic.IDE0053.severity = none # expression body lambda dotnet_diagnostic.IDE0046.severity = none # simplify if(s) - conditional operator dotnet_diagnostic.IDE0305.severity = none # [, ...] instead of .ToArray() +dotnet_diagnostic.IDE0306.severity = none # Use collection expressions # namespace declaration diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1ff0c42..0000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# 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/.github/workflows/Tests.yaml b/.github/workflows/Tests.yaml index dc174c8..2b71411 100644 --- a/.github/workflows/Tests.yaml +++ b/.github/workflows/Tests.yaml @@ -10,9 +10,8 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - project: [tests/Sharpify.Tests/Sharpify.Tests.csproj, tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj] uses: dusrdev/actions/.github/workflows/reusable-dotnet-test-mtp.yaml@main with: platform: ${{ matrix.platform }} - dotnet-version: 9.0.x - test-project-path: ${{ matrix.project }} \ No newline at end of file + dotnet-version: 10.0.x + test-project-path: tests/Sharpify.Tests/Sharpify.Tests.csproj \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd41f5b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# CHANGELOG + +* Only .NET 10+ support. +* Some computed properties in `BufferWrapper{T}` were made `readonly` to hint the compiler to not create defensive copies. +* `PersistentDictionary` in all of its variants have been removed - used `ArrowDb` instead. +* `SerializableObject`, `MonitoredSerializableObject` and `ThreadSafe` have all been combined into `Synchronized` which is much simpler, more performant. But can be more manual as it isn't coupled with `File.IO`. Instead it provides a delegate that can be provided and called on update. +* `Collections.IsNullOrEmpty` and all the other alias functions for `CollectionsMarshal` have been removed. +* `Utils` subclasses have been flattened and all the contents will reside directly in `Utils`. + * `Strings.FormatBytes` have been removed + * `Path` utilities were also removed - use `AppContext` instead. + * `String.IsNullOrEmpty|IsNullOrWhiteSpace|Concat` were also removed to enforce build-in language features. + * `TryConvertFromTo32` was also removed - `int.Parse|TryParse` are more than fast enough now. +* A struct `PooledArrayOwner{T}` was added to rent arrays from `ArrayPool{T}` without additional penalties. + * Extensions to match were added to any `ArrayPool{T}` including `Shared`, they allow you to get the owner and the array at the same time. `using var owner = ArrayPool{T}.Shared.Rent(minLength, out T[] array);` + * You can then proceed to use `BufferWrapper{T}.Create(array)` to get an `IBufferWriter` implementation over it. diff --git a/README.md b/README.md index 1ad8dfa..84239de 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,38 @@ # Sharpify -A collection of high performance language extensions for C#, fully compatible with NativeAOT - -## ⬇ Installation - -[![Nuget](https://img.shields.io/nuget/dt/Sharpify?label=Sharpify%20Nuget%20Downloads)](https://www.nuget.org/packages/Sharpify/) -> dotnet add package Sharpify - -[![Nuget](https://img.shields.io/nuget/dt/Sharpify.Data?label=Sharpify.Data%20Nuget%20Downloads)](https://www.nuget.org/packages/Sharpify.Data/) -> dotnet add package Sharpify.Data - -* `Sharpify.Data` is deprecated and will no longer be maintained. Refer to [ArrowDb](https://github.com/dusrdev/ArrowDb) for a superior alternative. - -[![Nuget](https://img.shields.io/nuget/dt/Sharpify.CommandLineInterface?label=Sharpify.CommandLineInterface%20Nuget%20Downloads)](https://www.nuget.org/packages/Sharpify.CommandLineInterface/) -> dotnet add package Sharpify.CommandLineInterface +[![NuGet](https://img.shields.io/nuget/v/Sharpify.svg?style=flat-square)](https://www.nuget.org/packages/Sharpify) +[![NuGet Downloads](https://img.shields.io/nuget/dt/Sharpify?style=flat&label=Downloads)](https://www.nuget.org/packages/Sharpify) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](License.txt) +[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) -## Sharpify - Base package - -`Sharpify` is a package mainly intended to extend the core language using high performance implementations. The other 2 packages uses `Sharpify` as a dependency. But its core features can be useful in a variety of applications by themselves. +A collection of high performance language extensions for C#, fully compatible with NativeAOT * ⚡ Fully Native AOT compatible * 🤷 `Either` - Discriminated union object that forces handling of both cases * 🦾 Flexible `Result` type that can encapsulate any other type and adds a massage options and a success or failure status. Flexible as it doesn't require any special handling to use (unlike `Either`) -* 🏄 Wrapper extensions that simplify use of common functions and advanced features from the `CollectionsMarshal` class * `Routine` and `AsyncRoutine` bring the user easily usable and configurable interval based background job execution. -* `PersistentDictionary` and derived types are super lightweight and efficient serializable dictionaries that are thread-safe and work amazingly for things like configuration files. * `SortedList` bridges the performance of `List` and order assurance of `SortedSet` -* `PersistentDictionary` and variants provide all simple database needs, with perfected performance and optimized concurrency. -* `SerializableObject` and the `Monitored` variant allow persisting an object to the disk, and elegantly synchronizing modifications. +* `Synchronized` is a thread-safe object owner with an optional delegate that can be executed on update. * 💿 `StringBuffer` enables zero allocation, easy to use appending buffer for creation of strings in hot paths. -* `RentedBufferWriter{T}` is an allocation friendly alternative to `ArrayBufferWriter{T}` for hot paths. +* `PooledArrayOwner{T}` is struct based alternative to `IMemoryOwner` with extensions built into the `ArrayPool` class. +* `BufferWrapper{T}` is a ref struct implementation of `IBufferWriter{T}` that wraps a `Span`. * A 🚣🏻 boat load of extension functions for all common types, bridging ease of use and performance. -* `Utils.DateAndTime`, `Utils.Env`, `Utils.Math`, `Utils.Strings` and `Utils.Unsafe` provide uncanny convenience at maximal performance. -* 🧵 `ThreadSafe` makes any variable type thread-safe +* A bunch of utils in `Utils` class. * 🔐 `AesProvider` provides access to industry leading AES-128 encryption with virtually no setup * 🏋️ High performance optimized alternatives to core language extensions -* 🎁 More added features that are not present in the core language -* ❗ Static inner exception throwers guide the JIT to further optimize the code during runtime. * 🫴 Focus on giving the user complete control by using flexible and common types, and resulting types that can be further used and just viewed. -For more information check [inner directory](src/Sharpify/README.md). - -## Sharpify.Data - -`Sharpify.Data` is an extension package, that should be installed on-top of `Sharpify` and adds a high performance persistent key-value-pair database, utilizing [MemoryPack](https://github.com/Cysharp/MemoryPack). The database support multiple types in the same file, 2 stage AES encryption (for whole file and per-key). Filtering by type, Single or Array value per key, and more... - -* `Database` is the base type for the data base, it is key-value-pair based local database - saved on disk. -* `IDatabaseFilter` is an interface which acts as an alternative to `DbContext` and provides enhanced type safety for contexts. -* `MemoryPackDatabaseFilter` is an implementation which focuses on types that implement `IMemoryPackable` from `MemoryPack`. -* `FlexibleDatabaseFilter` is an implementation focusing on types which need custom serialization logic. To use this, you type `T` will need to implement `IFilterable` which has methods for serialization and deserialization of single `T` and `T[]`. If you can choose to implement only one of the two. -* **Concurrency** - `Database` uses highly performant synchronous concurrency models and is completely thread-safe. -* **Disk Usage** - `Database` tracks inner changes and skips serialization if no changes occurred, enabling usage of periodic serialization without resource waste. -* **GC Optimization** - `Database` heavily uses pooling for encryption, decryption, type conversion, serialization and deserialization to minimize GC overhead, very rarely does it allocate single-use memory and only when absolutely necessary. -* **HotPath APIs** - `Database` is optimized for hot paths, as such it provides a number of APIs that specifically combine features for maximum performance and minimal GC overhead. Like the `TryReadToRentedBuffer` methods which is optimized for adding data to a table. -* **Runtime Optimization** - Upon initialization, `Database` chooses specific serializers and deserializers tailored for specific configurations, minimizing the amount of runnable code during runtime that would've been wasted on different checks. +## ⬇ Installation -For more information check [inner directory](src/Sharpify.Data/README.md). +> dotnet add package Sharpify ## Sharpify.CommandLineInterface `Sharpify.CommandLineInterface` is a standalone package that adds a high performance, reflection free and `AOT-ready` framework for creating command line and embedded interfaces -* Maintenance friendly model that depends on classes that implement `Command` or `SynchronousCommand` -* `Arguments` is an abstraction layer over the inputs that validate during runtime according to user needs via convenient APIs. -* Configuration using a fluent builder pattern. -* Configurable output and input pipes, enable usage outside of `Console` apps, supporting embedded use in any application. -* Automatic and structured general and command-specific help text. -* Configurable error handling with defaults. -* Super lightweight +It was moved to its own repo [Sharpify.CommandLineInterface](https://github.com/dusrdev/Sharpify.CommandLineInterface) -For more information check [inner directory](src/Sharpify.CommandLineInterface/README.md) +## ## Methodology diff --git a/Sharpify.slnx b/Sharpify.slnx index 808ae74..fd6cf51 100644 --- a/Sharpify.slnx +++ b/Sharpify.slnx @@ -1,6 +1,8 @@ - - - - + + + + + + diff --git a/src/Sharpify/CHANGELOG.md b/VERSIONS.md similarity index 95% rename from src/Sharpify/CHANGELOG.md rename to VERSIONS.md index 973dab4..dc8a5e8 100644 --- a/src/Sharpify/CHANGELOG.md +++ b/VERSIONS.md @@ -1,5 +1,21 @@ # CHANGELOG +## v3.0.0 + +* Only .NET 10+ support. +* Some computed properties in `BufferWrapper{T}` were made `readonly` to hint the compiler to not create defensive copies. +* `PersistentDictionary` in all of its variants have been removed - used `ArrowDb` instead. +* `SerializableObject`, `MonitoredSerializableObject` and `ThreadSafe` have all been combined into `Synchronized` which is much simpler, more performant. But can be more manual as it isn't coupled with `File.IO`. Instead it provides a delegate that can be provided and called on update. +* `Collections.IsNullOrEmpty` and all the other alias functions for `CollectionsMarshal` have been removed. +* `Utils` subclasses have been flattened and all the contents will reside directly in `Utils`. + * `Strings.FormatBytes` have been removed + * `Path` utilities were also removed - use `AppContext` instead. + * `String.IsNullOrEmpty|IsNullOrWhiteSpace|Concat` were also removed to enforce build-in language features. + * `TryConvertFromTo32` was also removed - `int.Parse|TryParse` are more than fast enough now. +* A struct `PooledArrayOwner{T}` was added to rent arrays from `ArrayPool{T}` without additional penalties. + * Extensions to match were added to any `ArrayPool{T}` including `Shared`, they allow you to get the owner and the array at the same time. `using var owner = ArrayPool{T}.Shared.Rent(minLength, out T[] array);` + * You can then proceed to use `BufferWrapper{T}.Create(array)` to get an `IBufferWriter` implementation over it. + ## v2.5.0 * Updated to support .NET 9.0 and optimized certain methods to use .NET 9 specific API's wherever possible. diff --git a/demos/Calc/Calc.sln b/demos/Calc/Calc.sln deleted file mode 100644 index 89dd4a2..0000000 --- a/demos/Calc/Calc.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calc", "Calc\Calc.csproj", "{FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/demos/Calc/Calc/Calc.csproj b/demos/Calc/Calc/Calc.csproj deleted file mode 100644 index f2ce22c..0000000 --- a/demos/Calc/Calc/Calc.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - Exe - net8.0 - enable - enable - true - true - - - - Calc - Calc - com.calc.demo.macos - 1.0.0 - Major - APPL - ???? - Calc - NSApplication - true - - - - - - Calc URL - Calc;Calc:// - - - - - - - - - diff --git a/demos/Calc/Calc/Commands/AddCommand.cs b/demos/Calc/Calc/Commands/AddCommand.cs deleted file mode 100644 index 555babb..0000000 --- a/demos/Calc/Calc/Commands/AddCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Calc.Commands; - -public class AddCommand : SynchronousCommand { - public override string Name => "Add"; - - public override string Description => "Add two numbers"; - - public override string Usage => "Add "; - - public override int Execute(Arguments args) { - if (!args.TryGetValue(0, default(int), out var a)) { - Console.WriteLine("Invalid number 1"); - return 1; - } - - if (!args.TryGetValue(1, 0, out var b)) { - Console.WriteLine("Number 2 defaulted to 0"); - // return 1; - } - - Console.WriteLine($"{a} + {b} = {a + b}"); - - return 0; - } -} \ No newline at end of file diff --git a/demos/Calc/Calc/Commands/DivideCommand.cs b/demos/Calc/Calc/Commands/DivideCommand.cs deleted file mode 100644 index 1d97c0c..0000000 --- a/demos/Calc/Calc/Commands/DivideCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Calc.Commands; - -public class DivideCommand : SynchronousCommand { - public override string Name => "Divide"; - - public override string Description => "Divide one number by another"; - - public override string Usage => "Divide "; - - public override int Execute(Arguments args) { - if (!args.TryGetValue(0, 0, out var a)) { - Console.WriteLine("Invalid number 1"); - return 1; - } - - if (!args.TryGetValue(1, 0, out var b) || b == 0) { - Console.WriteLine("Invalid number 2"); - return 1; - } - - Console.WriteLine($"{a} / {b} = {a / b}"); - - return 0; - } -} \ No newline at end of file diff --git a/demos/Calc/Calc/Commands/MultiplyCommand.cs b/demos/Calc/Calc/Commands/MultiplyCommand.cs deleted file mode 100644 index 6bf3c2b..0000000 --- a/demos/Calc/Calc/Commands/MultiplyCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Calc.Commands; - -public class MultiplyCommand : SynchronousCommand { - public override string Name => "Multiply"; - - public override string Description => "Multiply two numbers"; - - public override string Usage => "Multiply -a -b "; - - public override int Execute(Arguments args) { - if (!args.TryGetValue("a", 0, out var a)) { - Console.WriteLine("Invalid number 1"); - return 1; - } - - if (!args.TryGetValue("b", 0, out var b)) { - Console.WriteLine("Invalid number 2"); - return 1; - } - - Console.WriteLine($"{a} * {b} = {a * b}"); - - return 0; - } -} \ No newline at end of file diff --git a/demos/Calc/Calc/Commands/SubtractCommand.cs b/demos/Calc/Calc/Commands/SubtractCommand.cs deleted file mode 100644 index 8fed891..0000000 --- a/demos/Calc/Calc/Commands/SubtractCommand.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Calc.Commands; - -public class SubtractCommand : SynchronousCommand { - public override string Name => "Subtract"; - - public override string Description => "Subtract one number from another"; - - public override string Usage => - """ - Subtract [options] - Options: - --hex - Display the result in hexadecimal"; - """; - - public override int Execute(Arguments args) { - if (!args.TryGetValue(0, 0, out int a)) { - Console.WriteLine("Invalid number 1"); - return 1; - } - - if (!args.TryGetValue(1, 0, out int b)) { - Console.WriteLine("Invalid number 2"); - return 1; - } - - if (args.HasFlag("hex")) { - Console.WriteLine($"{a} - {b} = {a - b:X}"); - return 0; - } else { - Console.WriteLine($"{a} - {b} = {a - b}"); - } - - return 0; - } -} \ No newline at end of file diff --git a/demos/Calc/Calc/GlobalUsings.cs b/demos/Calc/Calc/GlobalUsings.cs deleted file mode 100644 index 67e7eb2..0000000 --- a/demos/Calc/Calc/GlobalUsings.cs +++ /dev/null @@ -1,4 +0,0 @@ -global using Sharpify.CommandLineInterface; -global using Sharpify; - -namespace Calc; \ No newline at end of file diff --git a/demos/Calc/Calc/Program.cs b/demos/Calc/Calc/Program.cs deleted file mode 100644 index b4b8a0e..0000000 --- a/demos/Calc/Calc/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Calc.Commands; - -namespace Calc; - -public class Program { - private static ReadOnlySpan Commands => new Command[] { - new AddCommand(), - new SubtractCommand(), - new MultiplyCommand(), - new DivideCommand() - }; - - static async Task Main(string[] args) { - var runner = CliRunner.CreateBuilder() - .AddCommands(Commands) - .SortCommandsAlphabetically() - .UseConsoleAsOutputWriter() - .WithMetadata(metadata => { - metadata.Name = "Calc"; - metadata.Description = "A simple calculator"; - metadata.Version = "1.0.0"; - metadata.Author = "Dave"; - metadata.License = "MIT"; - }) - .Build(); - - return await runner.RunAsync(args); - } -} \ No newline at end of file diff --git a/demos/Calc/README.md b/demos/Calc/README.md deleted file mode 100644 index 13ab2ee..0000000 --- a/demos/Calc/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Calc - -Calc is a demo to showcase the capabilities of `Sharpify.CommandLineInterface` by creating a NativeAot compatible calculator cli. - -## Guide - -This demo is accompanied by a YouTube video showcasing the package and this project, watch it here: - -[![Your new favorite CLI framework for C# - Sharpify.CommandLineInterface showcase](https://github.com/dusrdev/Sharpify/blob/main/demos/Calc/yt.video.thumbnail.png)](https://www.youtube.com/watch?v=bcuPY96Zr4k) - -## Map of file -> feature - -* `Program.cs` shows the main entry point and `CliRunner` -* `AddCommand.cs` shows positional and optional arguments -* `SubtractCommand.cs` shows positional arguments and flags -* `DivideCommand.cs` shows additional validation logic -* `MultiplyCommand.cs` shows how to use named arguments diff --git a/demos/Calc/yt.video.thumbnail.png b/demos/Calc/yt.video.thumbnail.png deleted file mode 100644 index f776454..0000000 Binary files a/demos/Calc/yt.video.thumbnail.png and /dev/null differ diff --git a/docs/10.-Sharpify.Data.md b/docs/10.-Sharpify.Data.md deleted file mode 100644 index 54f620b..0000000 --- a/docs/10.-Sharpify.Data.md +++ /dev/null @@ -1,54 +0,0 @@ -# Sharpify.Data - -For basic information check [this](https://github.com/dusrdev/Sharpify/blob/main/Sharpify.Data/README.md) - -## Usage Examples - -Lets see an high performance example, obviously, if you are using this package you care about performance, and here we appreciate that, a lot. - -### Initialization - -```csharp -using var database = Database.CreateOrLoad(new DatabaseConfiguration { - Path = path, // local - EncryptionKey = "mykey", - SerializeOnUpdate = true, - IgnoreCase = true -}); -``` - -### CRUD - -lets first create a value type that implements `IMemoryPackable` such as: - -```csharp -[MemoryPackable] -public readonly partial record struct Dog(string Species, int Age, float Weight); -``` - -Notice the `MemoryPackable` attribute from [MemoryPack](https://github.com/Cysharp/MemoryPack), this will implement the `IMemoryPackable` interface behind the scenes using a source generator. `Database` utilizes this to enable unrivaled performance. - -```csharp -database.Upsert("Brian", new("Bipedal Talking Dog", 20)); -Dog? brian = database.Get("Brian"); // Get returns null if the key doesn't exist -// because SerializeOnUpdate options was chosen, it will handle this automatically -// otherwise use .Serialize() or .SerializeAsync() -// We also use the filter -var table = Database.CreateMemoryPackFilter(); -table.Upsert("Buster", new Dog("Buster", 5)); -bool exists = table.TryGetValue("Buster", out Dog buster); -``` - -#### FlexibleDatabaseFilter{T} - -As an alternative to `MemoryPackable` the database also has an option to create a `FlexibleDatabaseFilter{T}`. This filter can be used when you want to implement easy access to the existing apis with a type that can't implement `IMemoryPackable{T}`, one example would be collections. - -For that the type itself, would need to implement the interface `IFilterable{T}`. If the type has the `MemoryPackable` attribute, it means the serializer knows how to handle it, it would be required to add practically a single line of code to each of the methods from `IFilterable{T}` to implement it. If some methods aren't required, you could simply `return null;`. - -When your type lets say `TCustom` implements `IFilterable`, the following method will become available: - -```csharp -var table = Database.CreateFlexibleFilter(); -``` - -this filter implements the same interface as the `MemoryPackFilter` and will support the same apis. To allow a very uniform and cohesive codebase. diff --git a/src/Sharpify.CommandLineInterface/ArgumentsAccess.cs b/src/Sharpify.CommandLineInterface/ArgumentsAccess.cs deleted file mode 100644 index eb05cfe..0000000 --- a/src/Sharpify.CommandLineInterface/ArgumentsAccess.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System.Globalization; - -namespace Sharpify.CommandLineInterface; - -public sealed partial class Arguments { - /// - /// Checks if the specified key exists in the arguments. - /// - /// The key to check. - /// True if the key exists, false otherwise. - public bool Contains(string key) => _arguments.ContainsKey(key); - - /// - /// Checks if the specified positional argument exists in the arguments. - /// - /// The positional argument to check. - /// True if the key exists, false otherwise. - public bool Contains(int position) => Contains(position.ToString()); - - /// - /// Checks if the specified flag is present in the arguments. - /// - /// The flag to check. - /// True if the flag is present and has no value; otherwise, false. - /// - /// This is not the same as as this also checks that the value is empty, which is not the case for named arguments that can also be detected by - /// - public bool HasFlag(string flag) => _arguments.TryGetValue(flag, out string? val) && val.Length is 0; - - /// - /// Tries to retrieve the value of a positional argument. - /// - /// The key to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// true if the key exists, false otherwise. - public bool TryGetValue(int position, out string value) => TryGetValue(position.ToString(), out value); - - /// - /// Tries to retrieve the value of a specified key in the arguments. - /// - /// The key to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// true if the key exists, false otherwise. - public bool TryGetValue(string key, out string value) { - if (!_arguments.TryGetValue(key, out var res)) { - value = string.Empty; - return false; - } - value = res; - return true; - } - - /// - /// Tries to retrieve a value of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// true if the key exists, false otherwise. - public bool TryGetValue(ReadOnlySpan keys, out string value) - => _arguments.TryGetValue(keys, out value); - - /// - /// Tries to retrieve the value of the positional argument in the arguments. - /// - /// The positional argument to check. - /// The default value to return if the key doesn't exist. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// - /// The default value makes it very easy to default to some value that is used later even if not provided, for example: a downloader may accept the number of parallel connections as a parameter, but it should always default to some number, so you could put it here. Saving a few lines of code for checking and reverting to default yourself. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValue(int position, T defaultValue, out T value) where T : IParsable => TryGetValue(position.ToString(), defaultValue, out value); - - /// - /// Tries to retrieve the value of a specified key in the arguments. - /// - /// The key to check. - /// The default value to return if the key doesn't exist. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// - /// The default value makes it very easy to default to some value that is used later even if not provided, for example: a downloader may accept the number of parallel connections as a parameter, but it should always default to some number, so you could put it here. Saving a few lines of code for checking and reverting to default yourself. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValue(string key, T defaultValue, out T value) where T : IParsable { - if (!TryGetValue(key, out string val)) { - value = defaultValue; - return false; - } - var wasParsed = T.TryParse(val, CultureInfo.CurrentCulture, out T? result); - if (!wasParsed) { - value = defaultValue; - return false; - } - value = result!; - return true; - } - - /// - /// Tries to retrieve the value of a either of specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// The default value to return if the key doesn't exist. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// - /// The default value makes it very easy to default to some value that is used later even if not provided, for example: a downloader may accept the number of parallel connections as a parameter, but it should always default to some number, so you could put it here. Saving a few lines of code for checking and reverting to default yourself. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValue(ReadOnlySpan keys, T defaultValue, out T value) where T : IParsable { - if (!TryGetValue(keys, out string val)) { - value = defaultValue; - return false; - } - var wasParsed = T.TryParse(val, CultureInfo.CurrentCulture, out T? result); - if (!wasParsed) { - value = defaultValue; - return false; - } - value = result!; - return true; - } - - /// - /// Returns the value of the argument, or default if it fails to parse or key didn't exist. - /// - /// - /// The key of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// The value of the argument - public T GetValue(string key, T defaultValue) where T : IParsable { - _ = TryGetValue(key, defaultValue, out var value); - return value; - } - - /// - /// Returns the value of the positional argument, or default if it fails to parse or key didn't exist. - /// - /// - /// The position of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// Value of the argument - public T GetValue(int position, T defaultValue) where T : IParsable { - _ = TryGetValue(position, defaultValue, out var value); - return value; - } - - /// - /// Returns the value of the either of the key aliases, or default if it fails to parse or key didn't exist. - /// - /// - /// - /// - /// - public T GetValue(ReadOnlySpan keys, T defaultValue) where T : IParsable { - _ = TryGetValue(keys, defaultValue, out var value); - return value; - } - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The positional argument to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// If the key doesn't exist or can't be parsed, the the default(TEnum) will be used in the out parameter, this overloads also implies that the enum will be parsed case-sensitive - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(int position, out TEnum value) where TEnum : struct, Enum => TryGetEnum(position.ToString(), default, false, out value); - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The positional argument to check. - /// Whether to ignore case in parsing the enum - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// If the key doesn't exist or can't be parsed, the default(TEnum) will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(int position, bool ignoreCase, out TEnum value) where TEnum : struct, Enum => TryGetEnum(position.ToString(), default, ignoreCase, out value); - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The positional argument to check. - /// The default value to return if the key doesn't exist. - /// Whether to ignore case in parsing the enum - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(int position, TEnum defaultValue, bool ignoreCase, out TEnum value) where TEnum : struct, Enum => TryGetEnum(position.ToString(), defaultValue, ignoreCase, out value); - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The key to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// If the key doesn't exist or can't be parsed, the default(TEnum) will be used in the out parameter, this overloads also implies that the enum will be parsed case-sensitive - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(string key, out TEnum value) where TEnum : struct, Enum => - TryGetEnum(key, default, false, out value); - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The key to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// Whether to ignore case in parsing the enum - /// - /// If the key doesn't exist or can't be parsed, the default(TEnum) will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(string key, bool ignoreCase, out TEnum value) where TEnum : struct, Enum => - TryGetEnum(key, default, ignoreCase, out value); - - /// - /// Tries to retrieve the enum value of a specified key in the arguments. - /// - /// The key to check. - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// The default value to return if the key doesn't exist. - /// Whether to ignore case in parsing the enum - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(string key, TEnum defaultValue, bool ignoreCase, out TEnum value) where TEnum : struct, Enum { - if (!TryGetValue(key, out string val)) { - value = defaultValue; - return false; - } - var wasParsed = Enum.TryParse(val, ignoreCase, out TEnum result); - if (!wasParsed) { - value = defaultValue; - return false; - } - value = result; - return true; - } - - /// - /// Tries to retrieve the enum value of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(ReadOnlySpan keys, out TEnum value) where TEnum : struct, Enum - => TryGetEnum(keys, default, false, out value); - - /// - /// Tries to retrieve the enum value of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// Whether to ignore case in parsing the enum - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(ReadOnlySpan keys, bool ignoreCase, out TEnum value) where TEnum : struct, Enum - => TryGetEnum(keys, default, ignoreCase, out value); - - /// - /// Tries to retrieve the enum value of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// The value of the argument ("" if doesn't exist - NOT NULL). - /// The default value to return if the key doesn't exist. - /// Whether to ignore case in parsing the enum - /// - /// If the key doesn't exist or can't be parsed, the default value will be used in the out parameter. - /// - /// true if the key exists, false otherwise. - public bool TryGetEnum(ReadOnlySpan keys, TEnum defaultValue, bool ignoreCase, out TEnum value) where TEnum : struct, Enum { - if (!TryGetValue(keys, out string val)) { - value = defaultValue; - return false; - } - var wasParsed = Enum.TryParse(val, ignoreCase, out TEnum result); - if (!wasParsed) { - value = defaultValue; - return false; - } - value = result; - return true; - } - - /// - /// Returns the enum value of a positional argument, or default if it fails to parse or didn't exist. - /// - /// - /// The position of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// The value of the argument - public TEnum GetEnum(int position, TEnum defaultValue) where TEnum : struct, Enum { - _ = TryGetEnum(position, defaultValue, false, out var value); - return value; - } - - /// - /// Returns the enum value of a positional argument, or default if it fails to parse or didn't exist. - /// - /// - /// The position of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// True to ignore case when parsing the argument - /// The value of the argument - public TEnum GetEnum(int position, TEnum defaultValue, bool ignoreCase) where TEnum : struct, Enum { - _ = TryGetEnum(position, defaultValue, ignoreCase, out var value); - return value; - } - - /// - /// Returns the enum value of a specified key, or default if it fails to parse or key didn't exist. - /// - /// - /// The key of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// The value of the argument - public TEnum GetEnum(string key, TEnum defaultValue) where TEnum : struct, Enum { - _ = TryGetEnum(key, defaultValue, false, out var value); - return value; - } - - /// - /// Returns the enum value of a specified key, or default if it fails to parse or key didn't exist. - /// - /// - /// The key of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// True to ignore case when parsing the argument - /// The value of the argument - public TEnum GetEnum(string key, TEnum defaultValue, bool ignoreCase) where TEnum : struct, Enum { - _ = TryGetEnum(key, defaultValue, ignoreCase, out var value); - return value; - } - - /// - /// Returns the value of the either of the key aliases, or default if it fails to parse or key didn't exist. - /// - /// - /// The keys of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// The value of the argument - public TEnum GetEnum(ReadOnlySpan keys, TEnum defaultValue) where TEnum : struct, Enum { - _ = TryGetEnum(keys, defaultValue, false, out var value); - return value; - } - - /// - /// Returns the value of the either of the key aliases, or default if it fails to parse or key didn't exist. - /// - /// - /// The keys of the argument to check - /// The default value to return if the argument doesn't exist or can't be parsed - /// True to ignore case when parsing the argument - /// The value of the argument - public TEnum GetEnum(ReadOnlySpan keys, TEnum defaultValue, bool ignoreCase) where TEnum : struct, Enum { - _ = TryGetEnum(keys, defaultValue, ignoreCase, out var value); - return value; - } -} diff --git a/src/Sharpify.CommandLineInterface/ArgumentsAccessMultiple.cs b/src/Sharpify.CommandLineInterface/ArgumentsAccessMultiple.cs deleted file mode 100644 index dca7db7..0000000 --- a/src/Sharpify.CommandLineInterface/ArgumentsAccessMultiple.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Globalization; - -namespace Sharpify.CommandLineInterface; - -public sealed partial class Arguments { - /// - /// Tries to retrieve the value of a positional argument. - /// - /// The key to check. - /// - /// The values of the argument or an empty array if they don't exist. - /// true if the key exists, false otherwise. - public bool TryGetValues(int position, string? separator, out string[] values) - => TryGetValues(position.ToString(), separator, out values); - - /// - /// Tries to retrieve the value of a specified key in the arguments. - /// - /// The key to check. - /// - /// The values of the argument or an empty array if don't exist. - /// true if the key exists, false otherwise. - public bool TryGetValues(string key, string? separator, out string[] values) { - if (!_arguments.TryGetValue(key, out var res)) { - values = []; - return false; - } - values = res.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - return true; - } - - /// - /// Tries to retrieve a values of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// - /// The values of the argument or an empty array if don't exist. - /// true if the key exists, false otherwise. - public bool TryGetValues(ReadOnlySpan keys, string? separator, out string[] values) { - if (!_arguments.TryGetValue(keys, out var res)) { - values = []; - return false; - } - values = res.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - return true; - } - - /// - /// Tries to retrieve the values of a specified key in the arguments. - /// - /// The positional argument to check. - /// - /// The values of the argument or an empty array if don't exist. - /// - /// - /// If the key doesn't exist or any of the values can't be parsed, an empty array will be used in the out parameter. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValues(int position, string? separator, out T[] values) where T : IParsable - => TryGetValues(position.ToString(), separator, out values); - - /// - /// Tries to retrieve the values of a specified key in the arguments. - /// - /// The key to check. - /// - /// The values of the argument or an empty array if don't exist. - /// - /// - /// If the key doesn't exist or any of the values can't be parsed, an empty array will be used in the out parameter. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValues(string key, string? separator, out T[] values) where T : IParsable { - if (!TryGetValue(key, out string val)) { - values = []; - return false; - } - - var parts = val.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var result = new T[parts.Length]; - int i = 0; - - foreach (var part in parts) { - if (!T.TryParse(part, CultureInfo.CurrentCulture, out T? parsed)) { - values = []; - return false; - } - result[i++] = parsed; - } - - values = result; - return true; - } - - /// - /// Tries to retrieve a values of one of a specified key's aliases in the arguments. - /// - /// A collection of aliases for a parameter name - /// - /// The values of the argument or an empty array if don't exist. - /// - /// - /// If the key doesn't exist or any of the values can't be parsed, an empty array will be used in the out parameter. - /// - /// - /// true if the key exists, false otherwise. - public bool TryGetValues(ReadOnlySpan keys, string? separator, out T[] values) where T : IParsable { - if (!TryGetValue(keys, out string val)) { - values = []; - return false; - } - - var parts = val.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var result = new T[parts.Length]; - int i = 0; - - foreach (var part in parts) { - if (!T.TryParse(part, CultureInfo.CurrentCulture, out T? parsed)) { - values = []; - return false; - } - result[i++] = parsed; - } - - values = result; - return true; - } -} diff --git a/src/Sharpify.CommandLineInterface/ArgumentsCore.cs b/src/Sharpify.CommandLineInterface/ArgumentsCore.cs deleted file mode 100644 index 2319aaa..0000000 --- a/src/Sharpify.CommandLineInterface/ArgumentsCore.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Sharpify.CommandLineInterface; - -/// -/// A wrapper class over a dictionary of string : string with additional features -/// -/// -/// Arguments instances are created via -/// -public sealed partial class Arguments { - /// - /// Source is the list of separated arguments on top of which this instance of was built. - /// - public readonly ReadOnlyCollection Source; - private readonly Dictionary _arguments; - - /// - /// Internal constructor for the class - /// - /// Copy or reference of the arguments before processing - /// Ensure not null or empty - internal Arguments(ReadOnlyCollection args, Dictionary arguments) { - Source = args; - _arguments = arguments; - } - - /// - /// Gets the number of arguments. - /// - public int Count => _arguments.Count; - - /// - /// Checks if the arguments are empty. - /// - public bool AreEmpty => _arguments.Count is 0; - - /// - /// Returns an empty arguments object. - /// - public static readonly Arguments Empty = new(Array.Empty().AsReadOnly(), []); - - /// - /// Returns an array copy of - /// - public string[] SourceCopy => Source.ToArray(); - - /// - /// Returns new Arguments with positional arguments forwarded by 1, so that argument that was 1 is now 0, 2 is now 1 and so on. This is non-destructive, the original arguments are not modified. - /// - /// - /// - /// This is useful if you have a command that has a sub-command and you want to pass the arguments to the sub-command - /// - /// - /// The first positional argument (0) will be skipped to actually forward - /// - /// - public Arguments ForwardPositionalArguments() { - if (!Contains("0")) { - return this; - } - var dict = new Dictionary(_arguments.Comparer); - - foreach ((string prevKey, string prevValue) in _arguments) { - // Handle non numeric - if (!int.TryParse(prevKey.AsSpan(), out int numericIndex)) { - dict.Add(prevKey, prevValue); - } - // Handle numeric - if (numericIndex is 0) { // forwarding means the previous 0 is lost - continue; - } - dict.Add((numericIndex - 1).ToString(), prevValue); // Add with the index reduced by 1. - } - - // Because this is a new dictionary, if pos 1, isn't found, 0 still won't be present - // So essentially 0 was forwarded to no longer exist - return new Arguments(Source, dict); - } - - /// - /// Returns the underlying dictionary - /// - public ReadOnlyDictionary GetInnerDictionary() => _arguments.AsReadOnly(); -} diff --git a/src/Sharpify.CommandLineInterface/CHANGELOG.md b/src/Sharpify.CommandLineInterface/CHANGELOG.md deleted file mode 100644 index 7cd7adf..0000000 --- a/src/Sharpify.CommandLineInterface/CHANGELOG.md +++ /dev/null @@ -1,115 +0,0 @@ -# CHANGELOG - -## Version 2.0.0 - -**WARNING:** This release may contain breaking changes. - -- The `Arguments` source collection has been rewritten as a `ReadOnlyCollection`, which cascaded into numerous changes: - - `Parser.ParseArguments` (collection-based overloads) now takes a generic `IList`. This is converted internally to a `ReadOnlyCollection`, which is used as the source for `Arguments`. - - As a result, `Parser.Split` now returns a `List`. - - The `Parser.SplitToList` method was removed, as it is no longer needed. - - `Arguments.ArgsAsMemory` and `Arguments.ArgsAsSpan` were also removed. To inspect the source, use `Arguments.Source` or obtain a copy as a `string[]` with `Arguments.SourceCopy`. - - The `CliRunner.RunAsync` overload that previously accepted a `ReadOnlySpan` now accepts an `IList` instead. This allows implicit casting from both `string[]` and `List`, which are the most common CLI inputs. -- `HelpText` generators now use a `StringBuilder` internally, replacing the previous custom buffer. Since help text generation typically occurs only once during a CLI's lifetime, any potential performance impact is minimal. This also removes some logical size restraints. -- Removed `Microsoft.SourceLink.Github` as it is now used implicitly. - -**These changes enable several improvements:** - -- `Sharpify` is no longer a required dependency of this package and has been removed. This package can now be installed as a standalone. -- Creating an `Arguments` object with `Parser.ParseArguments` is now much simpler. You can use it directly without commands to create minimal CLIs in `Program.cs`. This will be particularly useful with the upcoming `.NET 10` feature allowing direct execution of `.cs` files. (A demo video with examples and best practices will be released when this feature is available.) - -## Version 1.5.0 - -- Updated to support NET9 with `Sharpify` 2.5.0 -- Optimized path of `Arguments` forwarding when no positional arguments are present. - -## Version 1.4.0 - -- Optimized `Parser`: - - `Split` now rents a buffer the array pool by itself and returns a `RentedBufferWriter`, this enables greater flexibility in usage, and simplifies the code. - - Changed lower level array allocation code to use generalized api to optimize on more platforms. -- `Arguments.TryGetValue` and `Arguments.TryGetValues` now have overloads that accept a `ReadOnlySpan keys`, this overload enables much simpler retrieval of parameters that have aliases, for example you might want something like `--name` and `-n` to map to the same value. - - If you specify the aliases using the collections expression (i.e `["one", "two"]`), since .NET 8, the compiler will generate an inline array for that, which is very efficient, you don't need to create an array yourself. but if you wanted to to you could for example create a `static readonly ReadOnlySpan aliases => new[] { "one", "two" };` and pass that instead, the compiler optimizes such case by writing the values directly in the assembly. -- `CliBuilder` now has an option to configure arguments case sensitivity using `ConfigureArgumentsCaseHandling`, by default arguments are case insensitive to prioritize user experience. however, if you want to have parameters that are case sensitive, for instances where you need more short flags like `grep` you can opt in for this feature by setting it to be case sensitive. -- `CliBuilder` can now configure how to handle empty inputs with `ConfigureEmptyInputBehavior`, by default it will display the help text and exit, but you can configure it to attempt to proceed with handling the commands, if a single command is used and command name is set to not required, this will execute the command with empty args, otherwise it will display the appropriate error message. - - This is a change in behavior, as previously by default an error showing that no command was found was displayed, but seems that showing the help text in those situations is the more common approach in modern CLIs. -- Updated parsing to detect cases where arguments start with `-` and are not names of arguments, for example if you required a positional argument of type `int` and the input was a negative number (also starts with `-`), it would've been interpreted as a named argument, now it will be correctly interpreted as a positional argument. - - The rule now also checks if the first character following a `-` is a digit, if it is, it will not be marked as named argument. Which means - don't use argument names that start with digits (this is a bad practice in general). -- Help text no contains a special case for "version" and "--version" that will just display the version from metadata. - - Help text (from main) now has specialized structure for cases where you only have one command, instead of printing commands and descriptions, it will print the single command usage - the rest will of the whole cli (metadata) -- To support `--version` and add more customization options, now `Metadata` and `CustomHeader` are independent, and you can configure which is used for help text with `SetHelpTextSource(HelpTextSource)`. `Metadata` will be used by default. -- The help text portion that used to display instruction to get help text is now shorter and more concise. -- `Arguments` now has overload to directly get the values that correspond to `TryGetValue` overloads with default values, since defaults values can be returned if no key was found or failed to be parsed, In some case the actual reason is not important and only the value is needed so we now have `GetValue` for this exact reason. - -## Version 1.3.0 - -- `Arguments` now contains new methods `TryGetValues` and `TryGetValues{T}` to get arrays from values, there are overloads for regular and positional arguments, each overload requires a `string? separator` that is used to split the value, as with te regular values, `T` needs to implement `IParsable{T}`. -- `CliBuilder` now has a method `ShowErrorCodes` that will enable the error codes next to `CliRunner` error outputs, that was previously enabled by default, now it will hide them by default to provide a cleaner experience for users, but the builder now can easily configure this for testing, or if you still want the user to see them. - -## Version 1.2.2 - -- Rewritten core function of argument forwarding to fix issue that caused non-positional arguments to be removed, now named arguments and flags should not be affected by positional forwarding at all. - - Important note: the `Args` array that is stored within the `Arguments` object, is never modified and no matter how many positional forwarding iterations have been executed, it maintains the original arguments. -- Added `Arguments.HasFlag(string)` method that could be used to specifically checks for flags. - - Previously `Arguments.Contains(string)` could be used for this purpose, but it could also return `true` for a named argument, effectively allowing a false-positive. `HasFlag` prevents this by checking that if it exists, the value is empty, which could only be the case for flags. -- Increased buffer size for help-text generation to prevents issues with complex clis. - -### Usage Note - -In case you are writing a cli which has a complex tree to navigate on the way to the execution, such as nested commands, and any single command processing gets verbose, remember that it is possible to create a `CliRunner` at any point. - -This means that you can create objects for the nested commands, inside the top level command you could then forward the positional arguments (or not) if you choose, then use the same builder pattern with `CliRunner.CreateBuilder()...` and add the nested commands, then execute using the already parsed `Arguments` object as the `CliRunner.RunAsync` also has an overload that accepts `Arguments`. - -## Version 1.2.1 - -- Updated core to use `Sharpify` 2.0.0 -- small optimizations - -## Version 1.2.0 - -- `Arguments`'s internal copy of the parsed args is now an array, this change was necessary to avoid special cases where the backing array was garbage collected leaving a phantom view. To get a read only copy you can use `.ArgsAsSpan` or `.ArgsAsMemory` according to your preference or use case. -- Improved `Parser`'s mapping function's stability, and also further reworked it to allow positional arguments after named ones, now positional arguments can be anywhere. - - A special case that needs consideration before usage is switches, i.e boolean toggle parameters, as they look like named parameters without values. If such "switch" is followed by a regular value, it will be regarded as a named parameter and its value, as opposed to a switch and a positional argument. Keep this in mind when you decide the arrangement of input arguments, to ensure your input works as intended. - - Switches work well, either when they are followed by other named arguments, or other switches. For simplicity, it is best to leave them as the last arguments. -- Added a new `SynchronousCommand` as an alternative to `Command`, it is basically syntactic sugar that makes it so you can implement an `Execute` method instead, in which you can return an `int`, when `async` is not needed, this can save multiple lines of code that just wrap `int`s in `ValueTask.FromResult` which can be quite verbose. - -## Version 1.1.0 - -### Changes to `CliBuilder` - -- `DoNotIncludeMetadataInHelpText` was removed, instead it will not be included by default. `ModifyMetadata` was renamed to `WithMetadata` and if used, will modify the default `CliMetadata` and include it in the help text. -- Added `WithCustomHeader(string)` as an alternative to using `CliRunnerMetadata`, there will be no exception when both are used, but in that case, `CliRunnerMetadata` has priority and will be the only one displayed. -- Added `SortCommandsAlphabetically`, which if specified will sort the commands alphabetically by name in the general help text, other than the help text, it has virtually no affect. Not specifying this, gives you control over the order, it will be exactly in the order that you added the commands and order of existing collection (if you added any commands via a collection). - -### Changes to `Arguments` - -- Overloads of `TryGetValue` were modified to add an option to `ignoreCase`, to make it more user friendly and still adhere to parameter placement guidelines, more overloads were added. - -## Version 1.0.5 - -- Added a `ReadOnlyMemory{string}` which is a copy of the arguments split up before being parsed to `Arguments`, it can be retrieved by the `Arguments.PureArguments`, in special cases in which you might create a nested command structure, which requires a partial parsing, then secondary parsing within a command, this can be very powerful as you can create a secondary `CliRunner` and pass any subsequence of those arguments to recreate an input. -- Overloads of `Arguments.GetValue` which take an `int` as `positional argument`, now that parameter renamed to be `position` to better signify what the overloads mean, it is a rather cosmetic change, but nevertheless. -- Add a `Arguments.Contains(int)` overload to match with the rest of the methods and suit `positional arguments`. - -## Version 1.0.4 - -- Added missing line break in global help text -- If the single word help is entered, it will now be recognized in place of command name to return the global help text, instead of trying to be parsed as a command. - -## Version 1.0.3 - -- Updated `Sharpify` dependency and implemented usage of new APIs to aid in maintainability. -- Add `DoNotIncludeMetadataInHelpText()` in `CliBuilder` which removes the metadata inclusion in the general help text. - -## Version 1.0.2 - -- Removed thread-local `StringBuilder` from `CliRunner`, replaced all usages with `StringBuffer` from `Sharpify` - -## Version 1.0.1 - -- Updated `Sharpify` dependency -- Slightly improved performance of general help text generator - -## Version 1.0.0 - -Initial version - no changes diff --git a/src/Sharpify.CommandLineInterface/CHANGELOGLATEST.md b/src/Sharpify.CommandLineInterface/CHANGELOGLATEST.md deleted file mode 100644 index c99433f..0000000 --- a/src/Sharpify.CommandLineInterface/CHANGELOGLATEST.md +++ /dev/null @@ -1,19 +0,0 @@ -# CHANGELOG - -## Version 2.0.0 - -**WARNING:** This release may contain breaking changes. - -- The `Arguments` source collection has been rewritten as a `ReadOnlyCollection`, which cascaded into numerous changes: - - `Parser.ParseArguments` (collection-based overloads) now takes a generic `IList`. This is converted internally to a `ReadOnlyCollection`, which is used as the source for `Arguments`. - - As a result, `Parser.Split` now returns a `List`. - - The `Parser.SplitToList` method was removed, as it is no longer needed. - - `Arguments.ArgsAsMemory` and `Arguments.ArgsAsSpan` were also removed. To inspect the source, use `Arguments.Source` or obtain a copy as a `string[]` with `Arguments.SourceCopy`. - - The `CliRunner.RunAsync` overload that previously accepted a `ReadOnlySpan` now accepts an `IList` instead. This allows implicit casting from both `string[]` and `List`, which are the most common CLI inputs. -- `HelpText` generators now use a `StringBuilder` internally, replacing the previous custom buffer. Since help text generation typically occurs only once during a CLI's lifetime, any potential performance impact is minimal. This also removes some logical size restraints. -- Removed `Microsoft.SourceLink.Github` as it is now used implicitly. - -**These changes enable several improvements:** - -- `Sharpify` is no longer a required dependency of this package and has been removed. This package can now be installed as a standalone. -- Creating an `Arguments` object with `Parser.ParseArguments` is now much simpler. You can use it directly without commands to create minimal CLIs in `Program.cs`. This will be particularly useful with the upcoming `.NET 10` feature allowing direct execution of `.cs` files. (A demo video with examples and best practices will be released when this feature is available.) diff --git a/src/Sharpify.CommandLineInterface/CliBuilder.cs b/src/Sharpify.CommandLineInterface/CliBuilder.cs deleted file mode 100644 index c9f8fec..0000000 --- a/src/Sharpify.CommandLineInterface/CliBuilder.cs +++ /dev/null @@ -1,137 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// Represents a builder for a CliRunner. -/// -public sealed class CliBuilder { - private readonly CliRunnerConfiguration _config; - - internal CliBuilder() { - _config = new CliRunnerConfiguration(); - } - - /// - /// Adds a command to the CLI runner. - /// - /// - /// The same instance of - public CliBuilder AddCommand(Command command) { - _config.Commands.Add(command); - return this; - } - - /// - /// Adds commands to the CLI runner. - /// - /// - /// The same instance of - public CliBuilder AddCommands(ReadOnlySpan commands) { - _config.Commands.AddRange(commands); - return this; - } - - /// - /// Sets the output writer for the CLI runner. - /// - /// - /// The same instance of - public CliBuilder SetOutputWriter(TextWriter writer) { - CliRunner.SetOutputWriter(writer); - return this; - } - - /// - /// Sets the output writer for the CLI runner to be . - /// - /// The same instance of - public CliBuilder UseConsoleAsOutputWriter() { - CliRunner.SetOutputWriter(Console.Out); - return this; - } - - /// - /// Sorts the commands alphabetically. - /// - /// - /// This change only affects the functionality of the help text. - /// - /// The same instance of - public CliBuilder SortCommandsAlphabetically() { - _config.SortCommandsAlphabetically = true; - return this; - } - - /// - /// Add metadata - can be used to generate the general help text (Is the default source) - /// - /// - /// Configure the help text source with - /// - /// The same instance of - public CliBuilder WithMetadata(Action options) { - options(_config.MetaData); - return this; - } - - /// - /// Add a custom header - can be used instead of Metadata in the header of the help text - /// - /// - /// Configure the help text source with - /// - /// The same instance of - public CliBuilder WithCustomHeader(string header) { - _config.CustomHeader = header; - return this; - } - - /// - /// Sets the source of the general help text. - /// - /// Requested source of the help text. - /// The same instance of - public CliBuilder SetHelpTextSource(HelpTextSource source) { - _config.HelpTextSource = source; - return this; - } - - /// - /// Configures how the parser handles argument casing. - /// - /// - /// By default it is set to to improve user experience - /// - /// The same instance of - public CliBuilder ConfigureArgumentCaseHandling(ArgumentCaseHandling caseHandling) { - _config.ArgumentCaseHandling = caseHandling; - return this; - } - - /// - /// Show error codes next to the error messages. - /// - /// The same instance of - public CliBuilder ShowErrorCodes() { - _config.ShowErrorCodes = true; - return this; - } - - /// - /// Configures how the CLI runner handles empty input. - /// - /// The same instance of - public CliBuilder ConfigureEmptyInputBehavior(EmptyInputBehavior behavior) { - _config.EmptyInputBehavior = behavior; - return this; - } - - /// - /// Builds the CLI runner. - /// - public CliRunner Build() { - if (_config.Commands.Count is 0) { - throw new InvalidOperationException("No commands were added."); - } - return new CliRunner(_config); - } -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/CliMetadata.cs b/src/Sharpify.CommandLineInterface/CliMetadata.cs deleted file mode 100644 index 7690a5d..0000000 --- a/src/Sharpify.CommandLineInterface/CliMetadata.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// Contains metadata for a CLI application. -/// -public record CliMetadata { - /// - /// The name of the CLI application. - /// - public string Name { get; set; } = string.Empty; - - /// - /// The description of the CLI application. - /// - public string Description { get; set; } = string.Empty; - - /// - /// The version of the CLI application. - /// - public string Version { get; set; } = string.Empty; - - /// - /// The author of the CLI application. - /// - public string Author { get; set; } = string.Empty; - - /// - /// The license of the CLI application. - /// - public string License { get; set; } = string.Empty; - - /// - /// The default metadata for a CLI application. - /// - public static readonly CliMetadata Default = new() { - Name = "Interface", - Description = "Default description.", - Version = "1.0.0", - Author = "John Doe", - License = "MIT", - }; - - /// - /// Returns the total length of the metadata. - /// - public int TotalLength => Name.Length + Description.Length + Version.Length + Author.Length + License.Length; -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/CliRunner.cs b/src/Sharpify.CommandLineInterface/CliRunner.cs deleted file mode 100644 index dd4b52a..0000000 --- a/src/Sharpify.CommandLineInterface/CliRunner.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Collections.ObjectModel; -using System.Text; - -namespace Sharpify.CommandLineInterface; - -/// -/// Provides the means of running a CLI and configuring package wide settings. -/// -public sealed class CliRunner { - /// - /// Creates a new instance of the class. - /// - public static CliBuilder CreateBuilder() => new(); - - /// - /// Gets the commands registered with the CLI runner. - /// - public ReadOnlyCollection Commands => _config.Commands.AsReadOnly(); - - /// - /// Gets the output writer for the CLI runner. - /// - /// Defaults to - public static TextWriter OutputWriter { get; private set; } = TextWriter.Null; - - /// - /// Sets the output writer for the CLI runner. - /// - public static void SetOutputWriter(TextWriter writer) { - OutputWriter = writer; - } - - private readonly CliRunnerConfiguration _config; - - /// - /// Creates a new instance of the class. - /// - /// To be used with the - internal CliRunner(CliRunnerConfiguration config) { - _config = config; - // If there is only one command, sorting is not necessary - if (_config.SortCommandsAlphabetically && _config.Commands.Count is not 1) { - _config.Commands.Sort(Command.ByNameComparer); - } - } - - /// - /// Runs the CLI application with the specified arguments. - /// - public ValueTask RunAsync(ReadOnlySpan args, bool commandNameRequired = true) { - // Handle no input - if (args.Length is 0) { - // If display help text is used, always display the help text - if (_config.EmptyInputBehavior is EmptyInputBehavior.DisplayHelpText) { - return OutputHelper.Return(GenerateHelpText(commandNameRequired), 0); - } - // We assume the need to attempt to proceed - if (commandNameRequired || _config.Commands.Count is not 1) { - // In this case, input is required - return OutputHelper.Return("No command specified", 404, _config.ShowErrorCodes); - } - return _config.Commands[0].ExecuteAsync(Arguments.Empty); - } - var arguments = Parser.ParseArguments(args, _config.GetComparer()); - return RunAsync(arguments, commandNameRequired); - } - - /// - /// Runs the CLI application with the specified arguments. - /// - public ValueTask RunAsync(IList args, bool commandNameRequired = true) { - // Handle no input - if (args.Count is 0) { - // If display help text is used, always display the help text - if (_config.EmptyInputBehavior is EmptyInputBehavior.DisplayHelpText) { - return OutputHelper.Return(GenerateHelpText(commandNameRequired), 0); - } - // We assume the need to attempt to proceed - if (commandNameRequired || _config.Commands.Count is not 1) { - // In this case, input is required - return OutputHelper.Return("No command specified", 404, _config.ShowErrorCodes); - } - return _config.Commands[0].ExecuteAsync(Arguments.Empty); - } - var arguments = Parser.ParseArguments(args, _config.GetComparer()); - return RunAsync(arguments, commandNameRequired); - } - - /// - /// Runs the CLI application with the specified arguments. - /// - public ValueTask RunAsync(Arguments? arguments, bool commandNameRequired = true) { - if (arguments is null or { Count: 0 }) { - return OutputHelper.Return("Input could not be parsed", 400, _config.ShowErrorCodes); - } - - string version = $"Version: {_config.MetaData.Version}"; // cache version - - // general help text - if (arguments.IsFirstOrFlag("help")) { - return OutputHelper.Return(GenerateHelpText(commandNameRequired), 0); - } - if (arguments.IsFirstOrFlag("version")) { - return OutputHelper.Return(version, 0); - } - - // Only for single command CLIs - if (!commandNameRequired) { - // If there is more than one command, the command name is required - if (_config.Commands.Count is not 1) { - return OutputHelper.Return("Command name is required when using more than one command", 405, _config.ShowErrorCodes); - } - // Execute the command - return _config.Commands[0].ExecuteAsync(arguments); - } - - if (!arguments.TryGetValue(0, out string commandName)) { - return OutputHelper.Return("Command name is required", 405, _config.ShowErrorCodes); - } - - Command? command = _config.Commands.FirstOrDefault(c => _config.GetComparer().Equals(c.Name, commandName)); - if (command == default) { - return OutputHelper.Return($"Command \"{commandName}\" not found.", 404, _config.ShowErrorCodes); - } - - if (arguments.Contains("help") || arguments.HasFlag("help")) { - return OutputHelper.Return(command.GetHelp(), 0); - } - return command.ExecuteAsync(arguments.ForwardPositionalArguments()); - } - - // Generates the help for the application - happens once, at initialization of CliRunner - private string GenerateHelpText(bool commandNameRequired) { - StringBuilder builder = new(GetRequiredBufferLength()); - builder.AppendLine(); - if (_config.HelpTextSource is HelpTextSource.Metadata) { - var metaData = _config.MetaData; - builder.AppendLine(metaData.Name) - .AppendLine() - .AppendLine(metaData.Description) - .Append("Author: ") - .AppendLine(metaData.Author) - .Append("Version: ") - .AppendLine(metaData.Version) - .Append("License: ") - .AppendLine(metaData.License) - .AppendLine(); - } else if (_config.HelpTextSource is HelpTextSource.CustomHeader) { - builder.AppendLine(_config.CustomHeader) - .AppendLine(); - } - if (commandNameRequired) { - builder.AppendLine("Commands:"); - var maxCommandLength = GetMaximumCommandLength() + 2; - foreach (Command command in _config.Commands) { - builder.Append(command.Name.PadRight(maxCommandLength)) - .Append(" - ") - .AppendLine(command.Description); - } - builder.Append( - """ - - To get help for a command, use: " --help" - To get help for the application, use: "--help" - - """ - ); - } else { - var command = _config.Commands[0]; - builder.Append("Usage: ") - .AppendLine(command.Usage); - } - - return builder.ToString(); - } - - private int GetMaximumCommandLength() => _config.Commands.Max(c => c.Name.Length); - - private int GetRequiredBufferLength() { - int length = (_config.Commands.Count + 5) * 256; // default buffer for commands and possible extra text - return _config.HelpTextSource switch { - HelpTextSource.Metadata => length + _config.MetaData.TotalLength, - HelpTextSource.CustomHeader => length + _config.CustomHeader.Length, - _ => length - }; - } -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/CliRunnerConfiguration.cs b/src/Sharpify.CommandLineInterface/CliRunnerConfiguration.cs deleted file mode 100644 index beb3a93..0000000 --- a/src/Sharpify.CommandLineInterface/CliRunnerConfiguration.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// Represents the internal configuration of a CLI runner. -/// -internal sealed class CliRunnerConfiguration { - /// - /// The commands that the CLI runner can execute. - /// - public List Commands { get; set; } = []; - - /// - /// The metadata of the CLI runner. - /// - public CliMetadata MetaData { get; set; } = CliMetadata.Default; - - /// - /// The header to use in the help text. - /// - public string CustomHeader { get; set; } = string.Empty; - - /// - /// The source of the help text. - /// - public HelpTextSource HelpTextSource { get; set; } = HelpTextSource.Metadata; - - /// - /// Whether to sort commands alphabetically. - /// - /// - /// It is set to false by default - /// - public bool SortCommandsAlphabetically { get; set; } - - /// - /// Whether to show error codes in the help text. - /// - /// - /// It is set to false by default to improve user experience - /// - public bool ShowErrorCodes { get; set; } - - /// - /// Configures the case sensitivity of arguments parsing - /// - public ArgumentCaseHandling ArgumentCaseHandling { get; set; } = ArgumentCaseHandling.IgnoreCase; - - /// - /// Configures the behavior of the CLI runner when empty input is provided. - /// - public EmptyInputBehavior EmptyInputBehavior { get; set; } = EmptyInputBehavior.DisplayHelpText; -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/Command.cs b/src/Sharpify.CommandLineInterface/Command.cs deleted file mode 100644 index a3f0de5..0000000 --- a/src/Sharpify.CommandLineInterface/Command.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text; - -namespace Sharpify.CommandLineInterface; - -/// -/// Represents a command for a CLI application. -/// -public abstract class Command { - /// - /// Gets the name of the command. - /// - public abstract string Name { get; } - /// - /// Gets the description of the command. - /// - public abstract string Description { get; } - /// - /// Gets the usage of the command. - /// - public abstract string Usage { get; } - - /// - /// Executes the command. - /// - public abstract ValueTask ExecuteAsync(Arguments args); - - /// - /// Gets the help for the command. - /// - public virtual string GetHelp() { - var length = (Name.Length + Description.Length + Usage.Length) * 2; - StringBuilder builder = new(length); - builder.AppendLine() - .Append("Command: ") - .AppendLine(Name) - .AppendLine() - .Append("Description: ") - .AppendLine(Description) - .AppendLine() - .Append("Usage: ") - .AppendLine(Usage); - return builder.ToString(); - } - - /// - /// Compares two commands by their name. - /// - /// The first command to compare. - /// The second command to compare. - /// - /// A value indicating the relative order of the commands. - /// The return value is less than 0 if x.Name is less than y.Name, - /// 0 if x.Name is equal to y.Name, and greater than 0 if x.Name is greater than y.Name. - /// - public static int ByNameComparer(Command x, Command y) { - return string.Compare(x.Name, y.Name, StringComparison.Ordinal); - } -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/ConfigurationEnums.cs b/src/Sharpify.CommandLineInterface/ConfigurationEnums.cs deleted file mode 100644 index 3f1153f..0000000 --- a/src/Sharpify.CommandLineInterface/ConfigurationEnums.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// Controls how the CLI runner handles empty input. -/// -public enum EmptyInputBehavior { - /// - /// Displays the help text and exits. - /// - DisplayHelpText, - /// - /// Attempts to proceed with handling the commands. - /// - /// - /// If a single command is used and command name is set to not required, this will execute the command with empty args, - /// otherwise it will display the appropriate error message. - /// - AttemptToProceed, -} - -/// -/// Dictates the source of the general help text -/// -public enum HelpTextSource { - /// - /// Use the metadata to generate HelpText - /// - Metadata, - /// - /// Use the custom header to generate HelpText - /// - CustomHeader -} - -/// -/// Configures how to handle argument casing -/// -public enum ArgumentCaseHandling { - /// - /// Ignore argument case - /// - IgnoreCase, - /// - /// Sets the arguments parser to be case sensitive - /// - CaseSensitive -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/Extensions.cs b/src/Sharpify.CommandLineInterface/Extensions.cs deleted file mode 100644 index e6aa152..0000000 --- a/src/Sharpify.CommandLineInterface/Extensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -internal static class Helper { - /// - /// Tries to retrieve the value of the first specified key that exists in the dictionary. - /// - /// - /// - /// - /// - internal static bool TryGetValue(this Dictionary dict, ReadOnlySpan keys, out string value) { - foreach (var key in keys) { - if (dict.TryGetValue(key, out var res)) { - value = res!; - return true; - } - } - value = ""; - return false; - } - - /// - /// Checks if the first argument is the specified value or if it is a flag. - /// - /// - /// - /// - internal static bool IsFirstOrFlag(this Arguments args, string value) { - if (args.TryGetValue(0, out string? first) && first == value) { - return true; - } - return args.Count is 1 && args.HasFlag(value); - } - - internal static StringComparer GetComparer(this CliRunnerConfiguration config) - => config.ArgumentCaseHandling switch { - ArgumentCaseHandling.IgnoreCase => StringComparer.OrdinalIgnoreCase, - ArgumentCaseHandling.CaseSensitive => StringComparer.Ordinal, - _ => throw new ArgumentOutOfRangeException(nameof(config.ArgumentCaseHandling), config.ArgumentCaseHandling, null) - }; -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/OutputHelper.cs b/src/Sharpify.CommandLineInterface/OutputHelper.cs deleted file mode 100644 index d58619f..0000000 --- a/src/Sharpify.CommandLineInterface/OutputHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// Provides helper methods for outputting using -/// -public static class OutputHelper { - /// - /// Writes a line to the output writer. - /// - public static void WriteLine(string message) => CliRunner.OutputWriter.WriteLine(message); - - /// - /// Writes a message to the output writer. - /// - public static void Write(string message) => CliRunner.OutputWriter.Write(message); - - /// - /// Writes a line to the output writer and returns the specified code. - /// - /// The message to write. - /// The code to return. - /// Whether to append the code to the message. - /// A containing the specified code. - /// Using will append [Code: ] to - public static ValueTask Return(string message, int code, bool appendCode = false) { - var writer = CliRunner.OutputWriter; - writer.Write(message); - if (appendCode) { - writer.Write($" [Code: {code}]"); - } - writer.WriteLine(); - return ValueTask.FromResult(code); - } -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/Parser.cs b/src/Sharpify.CommandLineInterface/Parser.cs deleted file mode 100644 index 4fece04..0000000 --- a/src/Sharpify.CommandLineInterface/Parser.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Collections.ObjectModel; -using System.Runtime.CompilerServices; - -namespace Sharpify.CommandLineInterface; - -/// -/// Command line argument parser -/// -public static class Parser { - /// - /// The default starting capacity of argument buffers - /// - private const int DefaultBufferCapacity = 8; - - /// - /// Very efficiently splits an input into a List of strings, respects quotes - /// - /// - public static List Split(ReadOnlySpan str) { - List args = new(0); // Force usage of empty array - if (str.Length is 0) { - return args; - } - args.EnsureCapacity(DefaultBufferCapacity); - int i = 0; - while ((uint)i < (uint)str.Length) { - char c = str[i]; - if (char.IsWhiteSpace(c)) { - i++; - continue; - } - if (c is '"') { // everything without a quote block is a single item, regardless of spaces - str = str.Slice(i + 1); - int nextQuote = str.IndexOf('"'); - if (nextQuote is -1) { - break; - } - args.Add(new string(str.Slice(0, nextQuote))); - i = nextQuote + 1; - continue; - } - // next is a word - str = str.Slice(i); - int nextSpace = str.IndexOf(' '); - if (nextSpace <= 0) { // the last word, no spaces after - args.Add(new string(str)); - i = str.Length; - continue; - } - args.Add(new string(str.Slice(0, nextSpace))); - i = nextSpace + 1; - } - return args; - } - - /// - /// Parses a string into an object - /// - /// - /// - /// This overload uses - /// - public static Arguments ParseArguments(ReadOnlySpan str) => ParseArguments(str, StringComparer.OrdinalIgnoreCase); - - /// - /// Parses a string into an object - /// - /// - /// - public static Arguments ParseArguments(ReadOnlySpan str, StringComparer comparer) { - var args = Split(str); - return ParseArguments(args, comparer); - } - - /// - /// Parses a collection of strings into an object - /// - /// - /// - /// This overload uses - /// - public static Arguments ParseArguments(TList args) where TList : IList - => ParseArguments(args, StringComparer.OrdinalIgnoreCase); - - /// - /// Parses a collections of strings into arguments. - /// - /// - /// - [MethodImpl(MethodImplOptions.NoInlining)] - public static Arguments ParseArguments(TList args, StringComparer comparer) where TList : IList { - if (args.Count is 0) { - return Arguments.Empty; - } - var roc = new ReadOnlyCollection(args); - var results = MapArguments(roc, comparer); - return results.Count is 0 ? Arguments.Empty : new Arguments(roc, results); - } - - // Maps a ReadOnlyCollection of strings into a dictionary of arguments - internal static Dictionary MapArguments(ReadOnlyCollection args, StringComparer comparer) { - var length = args.Count; - var results = new Dictionary(length, comparer); - Span mapped = stackalloc bool[length]; - int i = 0; - - // Named arguments - while (i < length) { - var current = args[i]; - // This is positional argument, processed in the next loop - // values of named params are processed in the single iteration of the named parameter - if (!IsParameterName(current)) { - i++; - continue; - } - // This is parameter name (starts with either - or --) - int ii = 0; - while (current[ii] is '-') { // Skip the dashes - ii++; - } - var name = current.Substring(ii); // Parameter name without dashes - - // i + 1 == args.Length => checks if the next argument is available - // if not, then this is a switch (i.e. a named boolean toggle) - // IsParameterName(args[i + 1]) => checks if the next argument is a parameter - // if it is, then again, this is a switch - if (i + 1 == length || IsParameterName(args[i + 1])) { - results[name] = string.Empty; - mapped[i] = true; - i++; - continue; - } - // If the previous condition didn't take - // then this is the value of the named parameter - var value = args[i + 1]; - results[name] = value; - mapped[i] = mapped[i + 1] = true; - i += 2; - } - - int position = 0; - - // Positional arguments (mapped as {pos: value}) - // The positional arguments are mapped in the order they appear - // And the number of the positional argument - // A positional argument may have the key 0, even if it is the last enter argument (assuming other arguments are named or switches) - for (i = 0; i < length; i++) { - if (mapped[i]) { - continue; - } - results[position.ToString()] = args[i]; - position++; - mapped[i] = true; - } - - return results; - } - - // Checks whether a string starts with "-" - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool IsParameterName(ReadOnlySpan str) { - // check length - if (str.Length is 0) { - return false; - } - // check numeric + negative numeric (negative numeric could look like parameter name because of the dash) - if (char.IsDigit(str[0]) || char.IsDigit(str[str.LastIndexOf('-') + 1])) { - return false; - } - // not dash - not parameter - if (!str.StartsWith("-")) { - return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/README.md b/src/Sharpify.CommandLineInterface/README.md deleted file mode 100644 index d3165ee..0000000 --- a/src/Sharpify.CommandLineInterface/README.md +++ /dev/null @@ -1,230 +0,0 @@ -# Sharpify.CommandLineInterface - -`Sharpify.CommandLineInterface` is a high performance, reflection free and AOT-ready framework for creating command line interfaces, with a configurable output writer and no direct dependency to `System.Console` enabling it to be embedded, used with inputs from any source and output to any source. - -Most other command line frameworks in c# use `reflection` to provide their "magic" such as generating help text, and providing input validation, `Sharpify.CommandLineInterface` instead uses compile time implemented metadata and user guided validation. each command must implement the `Command` or `SynchronousCommand` abstract class, part of which will be to set the command metadata, the main entry `CliRunner` also has an application level metadata object that can be customized in the `CliBuilder` process, using those, `Sharpify.CommandLineInterface` can resolve and format that metadata to generate an output similar to the other frameworks. Each command's entry point is either `ExecuteAsync` or `Execute` which receive an input of type `Arguments` that can be used to retrieve, validate and parse arguments. - -## Usage - -### Implementing Commands - -To implement a command create a class that inherits from the abstract `Command`: - -```csharp -public sealed class EchoCommand : Command { - public override string Name => "echo"; - - public override string Description => "Echoes the specified message."; - - public override string Usage => "echo "; - - public override ValueTask ExecuteAsync(Arguments args) { - if (!args.TryGetValue("message", out string message)) { // Validation - // This example returns error code 400 (http bad request code) to signal client error - // Any code you want can obviously be used - return OutputHelper.Return("No message specified", 400, true); - } - return OutputHelper.Return(message, 0); - } -} -``` - -or `SynchronousCommand` - -```csharp -public sealed class EchoCommand : SynchronousCommand { - public override string Name => "echo"; - - public override string Description => "Echoes the specified message."; - - public override string Usage => "echo "; - - public override int Execute(Arguments args) { - if (!args.TryGetValue("message", out string message)) { // Validation - // This example returns error code 400 (http bad request code) to signal client error - // Any code you want can obviously be used - Console.WriteLine("No message specified"); - return 404; - } - Console.WriteLine(message); - return 0; - } -} -``` - -As you can see the properties set the metadata for the command at compile time, and when it comes time to resolve it, no `reflection` is needed. - -`ExecuteAsync` is returning a `ValueTask` allowing both synchronous and asynchronous code, we use the high performance `Arguments` which is an object that manages arguments parsed from the input for retrieving and validating parameters. `Execute` is a synchronous alternative that just reduces the need of verbosity from `ValueTask.FromResult(int)` when `async` is not needed. - -`OutputHelper.Return` is a helper method which outputs the message to customizable `TextWriter` in `CliRunner`, and returns the code (`int`) that is specified. - -### Program.cs (Or other entry point) - -```csharp -public static class Program { - static ReadOnlySpan Commands => new Command[] { - new EchoCommand(), - new OtherCommand(), // This is for example sake, but can be anything - }; - - public static Task Main(string[] args) { - var runner = CliRunner.CreateBuilder() - .AddCommands(Commands) - .UseConsoleAsOutputWriter() - .ModifyMetadata(metadata => { - metadata.Name = "MyCli"; - metadata.Descriptions = "MyCli Description"; - metadata.Author = "John Doe"; - metadata.Version = "1.0.0"; - metadata.License = "MIT" - }) - .Build(); - - return runner.RunAsync(args).AsTask(); - } -} -``` - -We can add commands one by one, or use `params []` and `ReadOnlySpan`, if you want, you can also dynamically create an array of `Command`s from the executing assembly or any other using `reflection` and pass it as an argument, however this will be subject to trimming and can affect AOT compatibility. - -Fluent API (builder pattern) is used to add the commands, set the output to the console (we can also set it to any `TextWriter`), and modify the global metadata that will be used for HelpText generation, and finally, build. - -Running the app with `RunAsync` parses the `args`, and handles `help` requests, both global and per command, it delegates the execution to the appropriate command and injects arguments. After parsing the command name (first argument), `RunAsync` will also trigger `Arguments.ForwardPositionalArguments`, which will remove the command name and shift the arguments, so you don't need to account for it inside the logic of the command. - -### Validation - -Validation is performed at runtime depending on the actual logic inside the `ExecuteAsync` or `Execute` methods in each command. You choose how to interpret or handle each argument. - -```csharp -public override int Execute(Arguments args) { - if (!args.TryGetValue("x", 20, out int x)) { - // This examples checks if arguments has a named argument by name "x" (-x or --x) - // And the value of this argument can be parsed as an integer. - // A default value of 20 is also supplied - // If the value is not found or can't be parsed, it will be set to the default value (20) - // otherwise the parsed value. - Console.WriteLine("X was not found or had an invalid format, setting it to default (20)"); - } - Console.WriteLine(x); - return 0; -} -``` - -Because you provide the actual type (no inference is needed), reflection is also not needed, thus, Native AOT compatibility is maintained without the possibility of trimming. With the consolidated APIs of `Arguments` you can validate and parse concisely with minimal verbosity. - -### Minimalistic Structure Without Command Classes - -As validation and parsing (the main pain points of CLI development) are manged through the `Arguments` object. You can use it directly if you don't need the global orchestration of `CliRunner`. - -Example: Imagine you wanted a one `.cs` file that will take 2 numbers and add them, here's how to do that: - -```csharp -using Sharpify.CommandLineInterface; -// using top level statements (>= .NET 5) Program.cs implicitly gets string[] args -Arguments arguments = Parser.ParseArguments(args); -// For the example we will decide that we expect named parameters x and y -if (!arguments.TryGetValue("x", 0, out int x)) { - Console.WriteLine("Parameter \"x\" is required."); - return 1; // 1 is a common code for error -} -if (!arguments.TryGetValue("y", 0, out int y)) { - Console.WriteLine("Parameter \"y\" is required."); - return 1; -} -// If we reached here, we validated and parsed x and y successfully -Console.WriteLine($"{x} + {y} = {x + y}"); -return 0; // 0 is a common success code -``` - -In this example, we created a functional CLI that validates the existence and parses 2 named parameters, and used them, all in 10 lines of code. - -### Arguments Key Logic - -`Arguments` is a key-value-pair wrapper around `Dictionary` which stores mapped arguments. To ensure a wide variety of applications, it parses arguments in the following way: - -* Positional arguments are retrieved and parsed by using the position as key, for example: for the first argument (not named or flag), it could be retrieved by the key "0" or simply the number 0. -* Named arguments are parsed as regular key and value, dashes are removed from the key. So "--n" or "-n", key is "n". (But without dashes "n" will be registered as value of positional argument) - * If a number is following a dash, it will be considered a numeric value, so don't use numbers as keys. -* Flags are like named arguments but whose value is empty, in order to avoid them being interpreted as named arguments, it is best practice to keep them *after* all the other parameters. - -To handle the above there are the following overload resolutions in `Arguments`: - -* `TryGetValue(int position, out string value)` - Will `.ToString()` the position and check the arguments. -* `TryGetValue(string key, out string value)` - Will check the arguments for the key. -* `HasFlag(string flag)` - Will check the arguments for the flag, so it will check both named key and that value is empty. -* `TryGetValue(ReadOnlySpan keys, out string value)` - Will check the arguments for the keys, so the first matching key will be returned. This can be used to work with aliases (such as "-f" or "--file", it will find whichever the user enters and parse the value into the same variable) -* `ContainsKey(string key)` - Will check the arguments for the key. The argument in this case can be a named argument or flag, this overload doesn't distinguish between them. -* `ContainsKey(int position)` - Will `.ToString()` the position and check if a positional argument exists. - -Arguments also support parameters which their value is a group of inputs, think of how the tool `rm` support any number of files, this is the same here. To further help working with these scenarios `TryGetValues` overloads accept a `separator` and return either a `string[]` or `T[]`. See all the options below. - -### Arguments - All methods - -```csharp -// CORE FUNCTIONS: -int Count; -bool AreEmpty; -static readonly Arguments Empty; -ReadOnlyMemory ArgsAsMemory(); // inner input - after parsing and before mapping -ReadOnlySpan ArgsAsSpan(); // same but as span -Arguments ForwardPositionalArguments(); // returns a new instance with the positional arguments forwarded -// So position 0 is deleted, and what was 1 becomes new 0, and so on. -// Non positional arguments are not affected. -ReadOnlyDictionary GetInnerDictionary(); // returns the inner dictionary (advanced, useful mostly for debugging) - -// SINGLE VALUE CHECKS: -bool Contains(string key); -bool Contains(int position); -bool HasFlag(string flag); -bool TryGetValue(int position, out string value); -bool TryGetValue(string key, out string value); -bool TryGetValue(ReadOnlySpan keys, out string value); -/// T : IParsable -bool TryGetValue(int position, T defaultValue, out T value); -bool TryGetValue(string key, T defaultValue, out T value); -bool TryGetValue(ReadOnlySpan keys, T defaultValue, out T value); -T GetValue(string key, T defaultValue); -T GetValue(int position, T defaultValue); -T GetValue(ReadOnlySpan keys, T defaultValue); -/// TEnum : struct, Enum -bool TryGetEnum(int position, out TEnum value); -bool TryGetEnum(int position, bool ignoreCase, out TEnum value); -bool TryGetEnum(int position, TEnum defaultValue, bool ignoreCase, out TEnum value); -bool TryGetEnum(string key, out TEnum value); -bool TryGetEnum(string key, bool ignoreCase, out TEnum value); -bool TryGetEnum(string key, TEnum defaultValue, bool ignoreCase, out TEnum value); -bool TryGetEnum(ReadOnlySpan keys, out TEnum value); -bool TryGetEnum(ReadOnlySpan keys, bool ignoreCase, out TEnum value); -bool TryGetEnum(ReadOnlySpan keys, TEnum defaultValue, bool ignoreCase, out TEnum value); -TEnum GetEnum(int position, TEnum defaultValue); -TEnum GetEnum(int position, TEnum defaultValue, bool ignoreCase); -TEnum GetEnum(string key, TEnum defaultValue); -TEnum GetEnum(string key, TEnum defaultValue, bool ignoreCase); -TEnum GetEnum(ReadOnlySpan keys, TEnum defaultValue); -TEnum GetEnum(ReadOnlySpan keys, TEnum defaultValue, bool ignoreCase); - -/// Multiple values (i.e. Arrays of values for single key) -bool TryGetValues(int position, string? separator, out string[] values); -bool TryGetValues(string key, string? separator, out string[] values); -bool TryGetValues(ReadOnlySpan keys, string? separator, out string[] values); -/// T : IParsable - Ensure to set the type for the out parameter -bool TryGetValues(int position, string? separator, out T[] values); -bool TryGetValues(string key, string? separator, out T[] values); -bool TryGetValues(ReadOnlySpan keys, string? separator, out T[] values); -``` - -### Custom Parsing - -`Parser` is a static class that provides the functionality of mapping inputs to an `Arguments` object, it also has a function of parsing an input such as string (or `ReadOnlySpan`) to a `List`, it is efficient and different than `string.Split()` since it splits both on space and quotes, giving quotes priority, so that whatever is within quotes, will remain a single string, regardless of how many spaces there are inside. This can be especially important if you need file names that could contain spaces, or any other text. - -`Parser` also has overloads for mapping arguments that configure a `StringComparer`, by default a `StringComparer.OrdinalIgnoreCase` is used, but whatever you prefer can be used instead. - -### Overloads of `CliRunner.RunAsync` - -`CliRunner.RunAsync` has overloads for `ReadOnlySpan` (string), `IList` (direct cast from `string[]` or `List`), and `Arguments`, giving you full control over your input, and even custom parsing. - -## Contact - -For bug reports, feature requests or offers of support/sponsorship contact - -> This project is proudly made in Israel 🇮🇱 for the benefit of mankind. diff --git a/src/Sharpify.CommandLineInterface/Sharpify.CommandLineInterface.csproj b/src/Sharpify.CommandLineInterface/Sharpify.CommandLineInterface.csproj deleted file mode 100644 index 8239efa..0000000 --- a/src/Sharpify.CommandLineInterface/Sharpify.CommandLineInterface.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net9.0;net8.0 - latest - enable - 2.0.0 - enable - true - David Shnayder - David Shnayder - MIT - CHANGELOGLATEST.md - True - Sharpify.CommandLineInterface - An standalone package focused on creating minimalistic AOT compatible command line interfaces - https://github.com/dusrdev/Sharpify - https://github.com/dusrdev/Sharpify - git - Extensions;HighPerformance;Cli;Parser;Interface;CommandLine - true - true - false - true - - - - - - - - - - - <_Parameter1>Sharpify.CommandLineInterface.Tests - - - - \ No newline at end of file diff --git a/src/Sharpify.CommandLineInterface/SynchronousCommand.cs b/src/Sharpify.CommandLineInterface/SynchronousCommand.cs deleted file mode 100644 index 6e78a39..0000000 --- a/src/Sharpify.CommandLineInterface/SynchronousCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Sharpify.CommandLineInterface; - -/// -/// An alternative to that runs synchronously. -/// -/// -/// This is syntactic sugar for wrapping returns from ExecuteAsync in ValueTask.FromResult -/// -public abstract class SynchronousCommand : Command { - /// - public override ValueTask ExecuteAsync(Arguments args) { - return ValueTask.FromResult(Execute(args)); - } - - /// - /// Executes the command. - /// - /// - /// Status code - public abstract int Execute(Arguments args); -} \ No newline at end of file diff --git a/src/Sharpify.Data/Build.txt b/src/Sharpify.Data/Build.txt deleted file mode 100644 index b25b581..0000000 --- a/src/Sharpify.Data/Build.txt +++ /dev/null @@ -1,10 +0,0 @@ -nuget: -dotnet clean -c Release -dotnet build -c Release -dotnet pack -c Release -p:SignAssembly="" - -dll: -dotnet build -c Release -p:SignAssembly="" - -docs: -git subtree push --prefix docs https://github.com/dusrdev/Sharpify.wiki.git master diff --git a/src/Sharpify.Data/CHANGELOG.md b/src/Sharpify.Data/CHANGELOG.md deleted file mode 100644 index c224b48..0000000 --- a/src/Sharpify.Data/CHANGELOG.md +++ /dev/null @@ -1,174 +0,0 @@ -# CHANGELOG - -## v2.6.0 - -* Updated to support NET9 -* Updated to use `Sharpify 2.5.0` and `MemoryPack 1.21.3` -* All `byte[]` value returning reads from the database, now return `ReadOnlyMemory` instead, previously, to maintain the integrity of the value, a copy was made and returned, because there wasn't any guarantee against modification, `ReadOnlyMemory` enforced this guarantee without creating a copy, if you just reading the data this is much more performant, and if you want to modify it, you can always create a copy at your own discretion. -* Decreased memory allocations for the `Func` based `Remove` method. -* Removed compiler directions that potentially could not allow the JIT compiler to perform Dynamic PGO. -* `Upsert{T}` overloads now have a `Func updateCondition` parameter that can be used to ensure that a condition is met before being updated, this is a feature of NoSQL databases that protects against concurrent writes overwriting each other. Now you can use this feature in `Sharpify.Data` as well. - * Of course this feature is also available in `UpsertMany{T}` overloads, and also in the overloads of the `JsonTypeInfo T`. - * To make it easier to see the result, these `Upsert` methods now return `bool`. - * `False` will only be returned IF ALL of the following conditions are met: - 1. Previous value was stored under this key - 2. The previous value was successfully deserialized with the right type - 3. The `updateCondition` was not met -* `Database` now tracks changes (additions, updates, removals) and compares them serialization events, to avoid serialization if no updates occurred since the previous serialization. - * This means that you can automate serialization without worrying about potential waste of resources, for example you could omit `SerializeOnUpdate` from the `DatabaseConfiguration`, then create a background task that serializes on a given interval for example with `Sharpify.Routines.Routine` or `Sharpify.Routines.AsyncRoutine`, and it will only actually serialize if updates occurred. This can significantly improve performance in cases where there are write peaks, but the database is mostly read from. -* You can now set the `Path` in the `DatabaseConfiguration` to an empty string `""` to receive an in-memory version of the database. -It still has serialization methods, but they don't perform any operations, they are essentially duds. -* `TryReadToRentedBuffer where T : IMemoryPackable` will now be able to retrieve the precise amount of needed space, so the size of the rented buffer will more accurately reflect the size of the data, this should help with dramatically improve performance when dealing with large objects. Before the buffer would've rented a capacity according to the length of the serialized object, meaning that the buffer was x times larger than needed when x is size(object) / size(byte). So the larger was each object, the `RentedBufferWriter` size would grow exponentially, now it grows linearly, maximizing efficiency. -* And minor optimizations (same as every other release 😜) - -## v2.5.0 - -* Updated to use version 2.2.0 of `Sharpify` and later, and `MemoryPack` 1.21.1 and later. -* Removed apis that were previously marked as `Obsolete` -* An overload with `ReadOnlySpan{byte}` was added to `Upsert`, enables using `Upsert` on other types, such as lists or even pooled arrays. Be careful when using the `byte[]` overload, as now it doesn't create copies and instead inserts the reference instead to improve performance where needed. **DO NOT USE** this overload with buffers which you only temporarily own, for those use the new `ReadOnlySpan{byte}` overload. -* `UpsertMany` now also has a `ReadOnlySpan{T}` accepting overload, it will not improve performance, but still adds flexibility, it doesn't replace the original `T[]` overload, it is an alternative. If the main `T[]` suits your context, as in you already have a fixed size array, it will actually be more performant. -* New method `Database.TryReadToRentedBuffer` now rents an appropriately sized `RentedBufferWriter{T}` and attempts to write the value to it, then return it. If it is successful, the result can be viewed with `RentedBufferWriter{T}.WrittenSpan` and other apis, if not successful (i.e key not found), a disabled `RentedBufferWriter{T}` will be returned, it can be checked with the `RentedBufferWriter{T}.IsDisabled` property. - * There is also an optional parameter for `reservedCapacity`, this will make sure the buffer has a matching amount free capacity after writing the data, an explanation of why this is useful will be lower down the page. - * There are overloads in `Database` for both `byte` and `T : IMemoryPackable{T}`, as well as methods in `MemoryPackableDatabaseFilter{T}` and `FlexibleDatabaseFilter{T}`. -* **POSSIBLY BREAKING** for those who use the `JSON` serialized `T` overloads, in the previous versions, the `JsonSerializer` was used to generate a `string`, which then passed to `MemoryPack` for secondary serialization. To improve performance, now `JsonSerializer` directly serializes to `byte[]` which means using these types is now both faster and more memory efficient than before. But if you try to read values with the new version which were serialized in the old version, there may be inconsistencies which may cause errors. - * If you encounter such issues, you may want to synchronize manually as follows: - * Read the values with `TryGetString`, then use `JsonSerializer.Deserialize` on the strings to get the actual values, you can then proceed to upsert those values with the `JSON` overloads on the same keys. -* Some of the new changes were also leveraged internally to improve performance/memory allocations in various places. - -## v2.4.1 - -* Updated to version 2.0.0 of `Sharpify`. - -If you use an older version of `Sharpify` this update is not a requirement, it mainly addresses a fix since `DecryptBytes` of `AesProvider` in `Sharpify` now has 2 overloads with 2 parameters, and the compiler seems to trim the wrong one, unless the optional parameter is specified. - -* Also `preFilter` in `Database.Remove()` was renamed to `keyPrefix` to better signify its purpose. this change doesn't alter behavior. - -## v2.4.0 - -* Added an overload for `Remove` which takes in a `Func keySelector`, this function is more optimized then using if you were to iterate yourself and call the old `Remove` as this one will execute serialization only once at the end, and only if removals actually happened (selector actually matched at least one key). - * The new `Remove` method also has an overload that accepts a `string? preFilter` as well, which can be used to only check keys that start with `preFilter` (the `keySelector` doesn't need to account for it, it will applied to a slice if the `preFilter` is matched), if left `null` it will be ignored. - * This addition was also propagated to both implementations of the `IDatabaseFilter`, i.e `MemoryPackDatabaseFilter` and `FlexibleDatabaseFilter`, both of which modify the incoming delegate to the use the filtered key, enabling simple delegate matches without relying on implementation details, they don't have the option to use a `preFilter` as they themselves use the statically generated type filters they create. -* To accommodate the `Remove` methods, `MemoryPackDatabaseFilter` and `FlexibleDatabaseFilter` now create a `public static readonly string KeyFilter` property, which is the prefix they append to the keys, this is used internally for `Remove` but perhaps the could help if you need to inherit from these classes and override the `Remove` method. - * Both of them also use `KeyFilter` internally to generate the filtered keys in a slightly more efficient way to before. - * The `static readonly` field that contained the generic type name was also removed as it was integrated into `KeyFilter` at with no additional cost. - -## v2.3.0 - -* The codebase was refactored and separated into smaller files, to make it much easier to work with. -* `Upserts` of all overloads and entry points will now throw an exception if the `value` is `null`. This change was made to ensure the integrity of `TryGetValue` (from all variants) as it checks nullability of the value to ensure the key exists. This is also no point to add null values, as they are not meaningful data, by enforcing not null, the code becomes simpler, and less error prone. -* Added `StringEncoding` choice to `DatabaseConfiguration`, it defaults to `UTF8`, but can also be `UTF16`, `UTF8` requires less memory in default cases, but `UTF16` can be more efficient if most of the strings are `Unicode`. -* The factory methods named `Create` and `CreateAsync` were renamed to `CreateOrLoad` and `CreateOrLoadAsync` respectively, which better explains exactly what they do at a glance. This should make more sense to code reviewers who are not familiar with the package. -* **Filtering** - * `IDatabaseFilter` which is the abstraction of the filters now has proxies for `Serialize` and `SerializeAsync` which previously couldn't be accessed via this layer, but may be required if `SerializeOnUpdate=false`. - * `DatabaseFilter where T : IMemoryPackable` was renamed to `MemoryPackDatabaseFilter`, and the Database method to create an instance was renamed from `Database.FilterByType` to `CreateMemoryPackFilter`. - * A new filter is introduced: `FlexibleDatabaseFilter where T : IFilterable`, which enables filtering on any type, without depending on `MemoryPack` implementation, for this an interface `IFilterable` was also added, the interface will require implementing a few methods which dictate how to serialize and deserialize the specific value type. The `FlexibleDatabaseFilter` inturn will use those implementation to provide the same experience. The filter can be created by `Database.CreateFlexibleFilter` -* All JSON based `T` overloads now require a `JsonTypeInfo` instead of the `JsonSerializerContext`, this change increases safety in cases where a `JsonSerializerContext` which didn't implement `T` would still be accepted and an exception would've been thrown at runtime, All the changes necessary at the client side are to add `.T` at the end of `JsonSerializerContext.Default` parameter. - -### Workaround for broken NativeAot support from MemoryPack - -As of writing this, MemoryPack's NativeAot support is broken, for any type that isn't already in their cached types, the `MemoryPackFormatterProvider` uses reflection to get the formatter, which fails in NativeAot. -As a workaround, we need to add the formatters ourselves, to do this, take any 1 static entry point, that activates before the database is loaded, and add this: - -```csharp -// for every T type that relies on MemoryPack for serialization, and their inheritance hierarchy -// This includes types that implement IMemoryPackable (i.e types that are decorated with MemoryPackable) -MemoryPackFormatterProvider.Register(); -// If the type is a collection or dictionary use the other corresponding overloads: -MemoryPackFormatterProvider.RegisterCollection(); -// or -MemoryPackFormatterProvider.RegisterDictionary(); -// and so on... -// for all overloads check peek the definition of MemoryPackFormatterProvider, or their Github Repo -``` - -**Note:** Make sure you don't create a new static constructor in those types, `MemoryPack` already creates those, you will need to find a different entry point. - -With this the serializer should be able to bypass the part using reflection, and thus work even on NativeAot. - -P.S. The base type of the Database is already registered the same way on its own static constructor. - -### Announcement - -Internal benchmarks already show a considerable performance improvement in the .NET8 version vs .NET7, and there are already multiple cases where the separate implementations have to be made in order to change to accommodate both versions, with .NET9 release approaching, more cases like this are expected, due the added complexity, .NET7 support will be dropped with the release of version 2.4.0 in the future. Codebases that are enable to migrate to newer .NET version will be forced to use older version of Sharpify.Data. - -.NET8 support will be maintained much longer since it is an LTS release. - -## v2.2.0 - -**Possibly BREAKING** This version changes the base types of `Database` from `ReadOnlyMemory` to `byte[]`, apparently the change using `ReadOnlyMemory` produced invalid results as if the underlying array disappeared, which left users of a empty memory which has phantom meta-data. -To ensure this doesn't happen now `byte[]` is used to make sure all of the data remains, to prevent ownership issues, methods which return the actual `byte[]` values, now instead return a copy (albeit an efficient one), to make sure the data integrity in the database is safe. Same goes for inserts, where previously the source was placed in the database, this might have caused it to be prematurely garbage collected, thus leaving the database with a phantom metadata, now those actions create a copy and store it instead. - -* As per the issues described above and their respective solutions, memory allocations should rise in some cases, however, it is a good trade-off for ensuring absolute integrity. -* Some users mentioned that as other databases, have options to query collections, ie, returning more than 1 to 1 item per key. It could greatly improve the usability of `Database` to have such a feature. In this version the feature is introduced. -`TryGetValue` where `T : IMemoryPackable`, now have `TryGetValues` overload, which returns a `T[]`, all under the same key. Respectively, an overload `UpsertMany` was added with the same type restrictions. This also works from `IDatabaseFilter`, these options should greatly improve useability when it comes to storing collections. -* Another possibly breaking change but one required for quality of life was to rename the `string value` overloads to `TryGetString` instead of `TryGetValue`, since they caused ambiguity which hindered the compilers ability to the infer the data type when using `var` which we all love. - -## v2.1.0 - -* `DatabaseFilter{T}` type was changed from `readonly struct` to `class`, and it now implements the `IDatabaseFilter{T}` interface. the internal `CreateKey` that in the default implementation uses the type name and `:` to create a "filtered" key, is now marked as virtual. So that it is possible to inherit from `DatabaseFilter{T}` and override `CreateKey` to either use a different template, or even add to it, for example if you have nested generics, such as `TMemoryPackable`, in which case the default `DatabaseFilter{T}` would not be able do distinguish between the inner generic, possibly causing issues with serialization and deserialization. The change to `class` also should be costly, as the database filter can be stored as a field as well, and used similarly to `dbContext` of other databases. In case overriding `CreateKey` is not enough, you can of course implement the whole `IDatabaseFilter{T}` interface if you so choose. - -* Small **breaking** change in `DatabaseFilter{T}`, the filter will now use a `:` delimiter between the type name and the key, this means you keys won't be found if were upserted using pre-change filters. This is unfortunate but necessary change in order to 1. enable better filtering of keys from the `Database`, enabling searching and using split to get the second portion of the key. and 2. to lay the groundwork of possibly implementing more filters in the future. - -* A temporary **FIX** to the **breaking** change in from the addition of the delimiter to `DatabaseFilter{T}` could be implemented rather easily using the new change from above, simply inherit from `DatabaseFilter{T}`, don't add anything, just override the `CreateKey` function to return `string.Concat(TName, key)`, this will behave exactly the same as the previous version of `DatabaseFilter{T}`. Nevertheless, I recommend this only as a temporary fix if you want to install the update but use existing data. at some point you should use the new feature. - -`DatabaseFilter{T}` was initially designed better in the context of APIs, no longer offering the `Get`, but instead using `TryGetValue`, while it might require 1 line of code more, when reading and writing the code, it is less ambiguous, before, a null or default result could indicate `not found`, `failed to deserialized`, and even upserted as null. Now if `TryGetValue` returns false there can only be one reason and that is that the key did not exist. - -* To improve this conciseness, `Database` now has `TryGetValue` offerings, for regular `ReadOnlyMemory{byte}` output, `IMemoryPackable{T}` and `string`. These are now the preferred APIs to use when retrieving values. -* The old `Get` variants of `Database` are now marked as `Deprecated` to signal they shouldn't be used. This was made to reduce the amount of breaking changes in this version, the `Get` variants will stay on as `Deprecated` until the next `Major` version, at which point they will be deleted. I hope this gives you enough time to "migrate". - -* `UpsertAsString` and `UpsertAsT`(JSON version), are now also named just `Upsert`, their overload is inferred from the type of the arguments as string is not `IMemoryPackable{T}` and the JSON `T` version requires a `JsonSerializerContext`. -* Also added `TryGetValue` overloads for JSON `T`, you will know them because they both require a `JsonSerializerContext`. apparently before this version you could only `Upsert` a JSON `T`, I apologize for the oversight. - -## v2.0.2 - -* Moved `_lock` acquiring statements into the `try-finally` blocks to handle `out-of-band` exceptions. Thanks [TheGenbox](https://www.reddit.com/user/TheGenbox/). -* Modified all internal `await` statements to use `ConfigureAwait(false)`, also thanks to [TheGenbox](https://www.reddit.com/user/TheGenbox/). - -## v2.0.1 - -* Added `ContainsKey` and `Remove` functions to `DatabaseFilter{T}` to give it essentially the same functionality scope as the `Database` itself. -* Removed `Flags` attribute from `DataChangeType` as it wasn't really a flag type, it can only be one thing. - -## v2.0.0 - BREAKING CHANGE - -The entire package has been reworked. - -* `Database{T}` was removed, and so was `DatabaseOptions` -* `Database` is now the only offer, and was tremendously optimized. -* GC pressure massively reduced due to very heavy usage of pooling. -* The options that were previously part of `DatabaseOptions` enum are now simple `bool`s in `DatabaseConfiguration` -* `string` value type upsert and get now uses `MemoryPack` for improved efficiency. -* the base value type is now a `ReadOnlyMemory` instead of `byte[]` enabling internal use of better apis and more optimization, client usage shouldn't change much because of implicit converters. -* `DatabaseFilter{T}` is a `readonly struct` filter of the `Database` based on a types that are `IMemoryPackable`, the filter should provide an AOT-friendly way to use multiple types in the same file, while ensuring no unforeseen `deserialization` issues occur because of key exists but with value of different type. `DatabaseFilter{T}` is simple and AOT-friendly because it does no changes to `Database` whatsoever, instead just wraps the key that it uses with a modification that includes the name of the type. It is only an abstraction. Nevertheless, this abstraction is very powerful as it takes no additional effort from the user, and allowing the user to create generic consumer classes, all of which use the same database, but injecting the filter instead of the database, making it virtually impossible that a generic class will try to use a value of a different type. -* Using `DatabaseFilter{T}` without per key encryption should perform on par with `Database{T}`, which allowed its removal due to deprecation. -* `DatabaseFilter{T}` is a lightweight struct that won't have a big performance impact when it is copied by value, but it is possible to negate even that by using the `ref` and `in` keywords in your own APIs -* An internal abstraction of `DatabaseSerializer`, which has multiple implementations that vary by the `DatabaseConfiguration`, is now created with the database initialization and reused, enabling both more efficient hot-paths for all serialization related functionality and usage of code that is specifically optimized per each variation of `DatabaseConfiguration`. -* More optimizations were made to allow JIT to branch eliminate parts of the manual `Serialize` and `SerializeAsync` based on the `DatabaseConfiguration`. -* Lower level APIs are now used internally to interact with the underlying data structure that should shave a few nanoseconds out of the CRUD operations. - -The deletion of `Database{T}` gave way to make `Database` a much greater and more customizable database, all the previous functionality was improved, and the usage `DatabaseFilter{T}` without per-key encryption, now offers a replacement to `Database{T}` which is more extensible to allow usage for multiple `{T}` types, where as `Database{T}` allowed a single type. - -The new changes mean that runtime deserialization exceptions are almost guaranteed if you try to use it on a pre-existing database. It is recommended to either start from fresh (Which is not a big issue if you used the database for caching), or perform some matching (the easiest of which will probably be to extract all the values using the `GetKeys` and `Get` functions, save them somewhere and delete the database, then upgrade, initialize a database with similar configuration and add all the values back) - -## v1.1.0 - -* **BREAKING** `UpsertAsT` functions signature changed to force usage of new parameter, which is a `JsonSerializerContext` that can serialize `T`. -* Simplified structures of inner data representation, slightly reducing assembly size. -* `Sharpify.Data` is now fully AOT compatible - -## v1.0.3 - -* Upgraded concurrency synchronization model of `Database` and `Database{T}` to get more accurate reads when other threads are writing. -* Both now implement `IDisposable` to release the resources of the synchronization and should be disposed of properly, but since they are designed to be used throughout the lifetime of the application, it isn't absolutely crucial to do this, and the implement finalizer should take care of this, if disposing of it from your end is inconvenient - -## v1.0.2 - -* Fixed issue where an exception would be thrown if `Upsert` overrides a key. it should by design override. - -## v1.0.1 - -* Further optimized both database, heavily utilizing array pooling for encryption - -## v1.0.0 - -* Initial release - check github for information diff --git a/src/Sharpify.Data/CHANGELOGLATEST.md b/src/Sharpify.Data/CHANGELOGLATEST.md deleted file mode 100644 index 7576a0d..0000000 --- a/src/Sharpify.Data/CHANGELOGLATEST.md +++ /dev/null @@ -1,45 +0,0 @@ -# CHANGELOG - -## v2.6.0 - -* Updated to support NET9 -* Updated to use `Sharpify 2.5.0` and `MemoryPack 1.21.3` -* All `byte[]` value returning reads from the database, now return `ReadOnlyMemory` instead, previously, to maintain the integrity of the value, a copy was made and returned, because there wasn't any guarantee against modification, `ReadOnlyMemory` enforced this guarantee without creating a copy, if you just reading the data this is much more performant, and if you want to modify it, you can always create a copy at your own discretion. -* Decreased memory allocations for the `Func` based `Remove` method. -* Removed compiler directions that potentially could not allow the JIT compiler to perform Dynamic PGO. -* `Upsert{T}` overloads now have a `Func updateCondition` parameter that can be used to ensure that a condition is met before being updated, this is a feature of NoSQL databases that protects against concurrent writes overwriting each other. Now you can use this feature in `Sharpify.Data` as well. - * Of course this feature is also available in `UpsertMany{T}` overloads, and also in the overloads of the `JsonTypeInfo T`. - * To make it easier to see the result, these `Upsert` methods now return `bool`. - * `False` will only be returned IF ALL of the following conditions are met: - 1. Previous value was stored under this key - 2. The previous value was successfully deserialized with the right type - 3. The `updateCondition` was not met -* `Database` now tracks changes (additions, updates, removals) and compares them serialization events, to avoid serialization if no updates occurred since the previous serialization. - * This means that you can automate serialization without worrying about potential waste of resources, for example you could omit `SerializeOnUpdate` from the `DatabaseConfiguration`, then create a background task that serializes on a given interval for example with `Sharpify.Routines.Routine` or `Sharpify.Routines.AsyncRoutine`, and it will only actually serialize if updates occurred. This can significantly improve performance in cases where there are write peaks, but the database is mostly read from. -* You can now set the `Path` in the `DatabaseConfiguration` to an empty string `""` to receive an in-memory version of the database. -It still has serialization methods, but they don't perform any operations, they are essentially duds. -* `TryReadToRentedBuffer where T : IMemoryPackable` will now be able to retrieve the precise amount of needed space, so the size of the rented buffer will more accurately reflect the size of the data, this should help with dramatically improve performance when dealing with large objects. Before the buffer would've rented a capacity according to the length of the serialized object, meaning that the buffer was x times larger than needed when x is size(object) / size(byte). So the larger was each object, the `RentedBufferWriter` size would grow exponentially, now it grows linearly, maximizing efficiency. -* And minor optimizations (same as every other release 😜) - -### Reminder: Workaround for broken NativeAot support from MemoryPack - -As of writing this, MemoryPack's NativeAot support is broken, for any type that isn't already in their cached types, the `MemoryPackFormatterProvider` uses reflection to get the formatter, which fails in NativeAot. -As a workaround, we need to add the formatters ourselves, to do this, take any 1 static entry point, that activates before the database is loaded, and add this: - -```csharp -// for every T type that relies on MemoryPack for serialization, and their inheritance hierarchy -// This includes types that implement IMemoryPackable (i.e types that are decorated with MemoryPackable) -MemoryPackFormatterProvider.Register(); -// If the type is a collection or dictionary use the other corresponding overloads: -MemoryPackFormatterProvider.RegisterCollection(); -// or -MemoryPackFormatterProvider.RegisterDictionary(); -// and so on... -// for all overloads check peek the definition of MemoryPackFormatterProvider, or their Github Repo -``` - -**Note:** Make sure you don't create a new static constructor in those types, `MemoryPack` already creates those, you will need to find a different entry point. - -With this the serializer should be able to bypass the part using reflection, and thus work even on NativeAot. - -P.S. The base type of the Database is already registered the same way on its own static constructor. diff --git a/src/Sharpify.Data/DataChangeType.cs b/src/Sharpify.Data/DataChangeType.cs deleted file mode 100644 index d4ff7d3..0000000 --- a/src/Sharpify.Data/DataChangeType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Sharpify.Data; - -/// -/// The type of changed that occurred on a key -/// -public enum DataChangeType : byte { - /// - /// A key was inserted or updated - /// - Upsert = 1 << 0, - /// - /// A key was removed - /// - Remove = 1 << 1 -} \ No newline at end of file diff --git a/src/Sharpify.Data/DataChangedEventArgs.cs b/src/Sharpify.Data/DataChangedEventArgs.cs deleted file mode 100644 index 6555c79..0000000 --- a/src/Sharpify.Data/DataChangedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Sharpify.Data; - -/// -/// Event arguments for data changed (addition, update or removal of keys) -/// -public sealed class DataChangedEventArgs : EventArgs { - /// - /// The key that was changed - /// - public required string Key { get; init; } - - /// - /// The value that was changed - /// - public required object? Value { get; init; } - - /// - /// The type of change that occurred - /// - public required DataChangeType ChangeType { get; init; } -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseBase.cs b/src/Sharpify.Data/DatabaseBase.cs deleted file mode 100644 index 6ac4d38..0000000 --- a/src/Sharpify.Data/DatabaseBase.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Collections.Concurrent; - -using MemoryPack; - -using Sharpify.Data.Serializers; - -namespace Sharpify.Data; - -/// -/// A high performance database that stores String:byte[] pairs. -/// -/// -/// Do not create this class directly or by using an activator, the factory methods are required for proper initializations using different abstractions. -/// -public sealed partial class Database : IDisposable { - /// - /// The unique identifier of the database. - /// - public readonly Guid Guid = Guid.NewGuid(); - - private readonly Dictionary _data; - - private readonly ConcurrentQueue> _queue = new(); - - private volatile bool _disposed; - - // The updates count increments every time a value is updated, added or removed. - private long _updatesCount = 0; - - // The serialization reference is checking against the updates to reduce redundant serialization. - private long _serializationReference = 0; - -#if NET9_0_OR_GREATER - private readonly Lock _sLock = new(); -#else - private readonly object _sLock = new(); -#endif - private readonly ReaderWriterLockSlim _lock = new(); - private readonly AbstractSerializer _serializer; - private volatile int _estimatedSize; - - private const int BufferMultiple = 4096; - private const int ReservedBufferSize = 256; - - private readonly bool _isInMemory; - - /// - /// Overestimated size of the database. - /// - private int GetOverestimatedSize() { - return (int)Math.Ceiling((_estimatedSize + ReservedBufferSize) / (double)BufferMultiple) * BufferMultiple; - } - - /// - /// Holds the configuration for this database. - /// - public readonly DatabaseConfiguration Config; - - /// - /// Triggered when there is a change in the database. - /// - public event DataChangedEventHandler? DataChanged; - - /// - /// Represents the method that will handle the data changed event. - /// - /// The source of the event. - /// An instance of the DataChangedEventArgs class that contains the event data. - public delegate void DataChangedEventHandler(object sender, DataChangedEventArgs e); - - private void InvokeDataEvent(DataChangedEventArgs e) { - DataChanged?.Invoke(this, e); - } - - /// - /// Creates a high performance database that stores string-byte[] pairs. - /// - public static Database CreateOrLoad(DatabaseConfiguration config) { - AbstractSerializer serializer = AbstractSerializer.Create(config); - - if (!File.Exists(config.Path)) { - return config.IgnoreCase - ? new Database(new Dictionary(StringComparer.OrdinalIgnoreCase), config, serializer, 0) - : new Database(new Dictionary(), config, serializer, 0); - } - - int estimatedSize = Helper.GetFileSize(config.Path); - - Dictionary dict = serializer.Deserialize(estimatedSize); - - return new Database(dict, config, serializer, estimatedSize); - } - - /// - /// Creates asynchronously a high performance database that stores string-byte[] pairs. - /// - public static async ValueTask CreateOrLoadAsync(DatabaseConfiguration config, CancellationToken token = default) { - AbstractSerializer serializer = AbstractSerializer.Create(config); - - if (!File.Exists(config.Path)) { - return config.IgnoreCase - ? new Database(new Dictionary(StringComparer.OrdinalIgnoreCase), config, serializer, 0) - : new Database(new Dictionary(), config, serializer, 0); - } - - int estimatedSize = Helper.GetFileSize(config.Path); - - Dictionary dict = await serializer.DeserializeAsync(estimatedSize, token); - - return new Database(dict, config, serializer, estimatedSize); - } - - private Database(Dictionary data, DatabaseConfiguration config, AbstractSerializer serializer, int estimatedSize) { - _data = data; - Config = config; - _serializer = serializer; - _isInMemory = config.Path.Length == 0; - Interlocked.Exchange(ref _estimatedSize, estimatedSize); - } - - static Database() { - MemoryPackFormatterProvider.RegisterDictionary, string, byte[]>(); - } - - /// - /// Returns the amount of entries in the database. - /// - public int Count => _data.Count; - - /// - /// Returns a that can be used to filter the database by type. - /// - /// - /// - public IDatabaseFilter CreateMemoryPackFilter() where T : IMemoryPackable => new MemoryPackDatabaseFilter(this); - - /// - /// Returns a that can be used to filter the database by type. - /// - /// - /// - public IDatabaseFilter CreateFlexibleFilter() where T : IFilterable => new FlexibleDatabaseFilter(this); - - /// - /// Returns an immutable copy of the keys in the inner dictionary - /// - public IReadOnlyCollection GetKeys() { - try { - _lock.EnterReadLock(); - return _data.Keys; - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Frees the resources used by the database. - /// - public void Dispose() { - if (_disposed) { - return; - } - _lock.Dispose(); - _disposed = true; - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseConfiguration.cs b/src/Sharpify.Data/DatabaseConfiguration.cs deleted file mode 100644 index eecfbda..0000000 --- a/src/Sharpify.Data/DatabaseConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using MemoryPack; - -namespace Sharpify.Data; - -/// -/// Configuration for -/// -public record DatabaseConfiguration { - /// - /// The path to which the database file will be saved. - /// - /// - /// Setting path to an empty string "" will create an in-memory database. - /// - public required string Path { get; init; } - - /// - /// Whether the database keys case should be ignored. - /// - /// - /// This impacts performance on reads and deserialization. - /// - public bool IgnoreCase { get; init; } = false; - - /// - /// The encoding to use when serializing and deserializing strings in the database. - /// - public StringEncoding Encoding { get; init; } = StringEncoding.Utf8; - - /// - /// Whether to serialize the database automatically when it is updated. - /// - /// - /// This relates to adding, removing, and updating values. - /// - public bool SerializeOnUpdate { get; init; } = false; - - /// - /// Whether to trigger update events when the database is updated. - /// - /// - /// This relates to adding, removing, and updating values. - /// - public bool TriggerUpdateEvents { get; init; } = false; - - /// - /// General encryption key, the entire file will be encrypted with this. - /// - public string EncryptionKey { get; init; } = string.Empty; - - /// - /// Whether general encryption is enabled. - /// - public bool HasEncryption => EncryptionKey.Length > 0; -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseReads.cs b/src/Sharpify.Data/DatabaseReads.cs deleted file mode 100644 index 26d29a3..0000000 --- a/src/Sharpify.Data/DatabaseReads.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data; - -public sealed partial class Database { - /// - /// Checked whether the inner dictionary contains the . - /// - /// - public bool ContainsKey(string key) => _data.ContainsKey(key); - - /// - /// Tries to get the value for the . - /// - /// - /// - /// True if the value was found, false if not. - public bool TryGetValue(string key, out ReadOnlyMemory value) => TryGetValue(key, "", out value); - - /// - /// Tries to get the value for the . - /// - /// - /// individual encryption key for this specific value - /// - /// True if the value was found, false if not. - public bool TryGetValue(string key, string encryptionKey, out ReadOnlyMemory value) { - try { - _lock.EnterReadLock(); - // Get val reference - if (!_data.TryGetValue(key, out byte[]? val)) { // Not found - value = ReadOnlyMemory.Empty; - return false; - } - if (encryptionKey.Length is 0) { // Not encrypted - value = val ?? ReadOnlyMemory.Empty; - return true; - } - // Encrypted -> Decrypt - ReadOnlySpan valSpan = val ?? ReadOnlySpan.Empty; - value = Helper.Instance.Decrypt(valSpan, encryptionKey); - return true; - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, int reservedCapacity = 0) { - return TryReadToRentedBuffer(key, string.Empty, reservedCapacity); - } - - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) { - try { - _lock.EnterReadLock(); - // Get val reference - if (!_data.TryGetValue(key, out byte[]? val)) { // Not found - return new RentedBufferWriter(0); - } - if (encryptionKey.Length is 0) { // Not encrypted - var buffer = new RentedBufferWriter(val!.Length + reservedCapacity); - buffer.WriteAndAdvance(val); - return buffer; - } else { - ReadOnlySpan valSpan = val; - var buffer = new RentedBufferWriter(valSpan.Length + AesProvider.ReservedBufferSize + reservedCapacity); - int numWritten = Helper.Instance.Decrypt(valSpan, buffer.Buffer, encryptionKey); - buffer.Advance(numWritten); - return buffer; - } - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Tries to get the value for the . - /// - /// The type of object to retrieve. - /// The key used to identify the object in the database. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValue(string key, out T value) where T : IMemoryPackable { - return TryGetValue(key, string.Empty, out value); - } - - - /// - /// Tries to get the value for the . - /// - /// The type of object to retrieve. - /// The key used to identify the object in the database. - /// The encryption key used to decrypt the object if it is encrypted. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValue(string key, string encryptionKey, out T value) where T : IMemoryPackable { - try { - _lock.EnterReadLock(); - // Get val reference - if (!_data.TryGetValue(key, out byte[]? val)) { // Not found - value = default!; - return false; - } - ReadOnlySpan valSpan = val; - if (encryptionKey.Length is 0) { // Not encrypted - value = MemoryPackSerializer.Deserialize(valSpan, _serializer.SerializerOptions)!; - return true; - } - // Encrypted -> Decrypt - using var buffer = new RentedBufferWriter(valSpan.Length + AesProvider.ReservedBufferSize); - int length = Helper.Instance.Decrypt(valSpan, buffer.GetSpan(), encryptionKey); - buffer.Advance(length); - if (length is 0) { - value = default!; - return false; - } else { - value = MemoryPackSerializer.Deserialize(buffer.WrittenSpan, _serializer.SerializerOptions)!; - return true; - } - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Tries to get the value array stored in . - /// - /// The type of object to retrieve. - /// The key used to identify the object in the database. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValues(string key, out T[] value) where T : IMemoryPackable { - return TryGetValues(key, string.Empty, out value); - } - - - /// - /// Tries to get the value array stored in . - /// - /// The type of object to retrieve. - /// The key used to identify the object in the database. - /// The encryption key used to decrypt the object if it is encrypted. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValues(string key, string encryptionKey, out T[] values) where T : IMemoryPackable { - try { - _lock.EnterReadLock(); - // Get val reference - if (!_data.TryGetValue(key, out byte[]? val)) { // Not found - values = Array.Empty(); - return false; - } - ReadOnlySpan valSpan = val; - if (encryptionKey.Length is 0) { // Not encrypted - values = MemoryPackSerializer.Deserialize(valSpan, _serializer.SerializerOptions)!; - return true; - } - // Encrypted -> Decrypt - using var buffer = new RentedBufferWriter(valSpan.Length + AesProvider.ReservedBufferSize); - int length = Helper.Instance.Decrypt(valSpan, buffer.GetSpan(), encryptionKey); - buffer.Advance(length); - if (length is 0) { - values = default!; - return false; - } else { - values = MemoryPackSerializer.Deserialize(buffer.WrittenSpan, _serializer.SerializerOptions)!; - return true; - } - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, int reservedCapacity = 0) where T : IMemoryPackable { - return TryReadToRentedBuffer(key, string.Empty, reservedCapacity); - } - - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) where T : IMemoryPackable { - if (!TryGetValue(key, encryptionKey, out ReadOnlyMemory data)) { - return new RentedBufferWriter(0); - } - int dataLength = Helper.GetRequiredLength(data.Span); - int bufferLength = dataLength + reservedCapacity; - var buffer = new RentedBufferWriter(bufferLength); - Helper.ReadToRenterBufferWriter(ref buffer, data.Span, dataLength); - return buffer; - } - - /// - /// Tries to get the value for the . - /// - /// The key used to identify the object in the database. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetString(string key, out string value) { - return TryGetString(key, string.Empty, out value); - } - - - /// - /// Tries to get the value for the . - /// - /// The key used to identify the object in the database. - /// The encryption key used to decrypt the object if it is encrypted. - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetString(string key, string encryptionKey, out string value) { - try { - _lock.EnterReadLock(); - // Get val reference - if (!_data.TryGetValue(key, out byte[]? val)) { // Not found - value = ""; - return false; - } - ReadOnlySpan valSpan = val; - if (encryptionKey.Length is 0) { // Not encrypted - value = MemoryPackSerializer.Deserialize(valSpan, _serializer.SerializerOptions)!; - return true; - } - // Encrypted -> Decrypt - using var buffer = new RentedBufferWriter(valSpan.Length + AesProvider.ReservedBufferSize); - int length = Helper.Instance.Decrypt(valSpan, buffer.GetSpan(), encryptionKey); - buffer.Advance(length); - if (length is 0) { - value = ""; - return false; - } else { - value = MemoryPackSerializer.Deserialize(buffer.WrittenSpan, _serializer.SerializerOptions)!; - return true; - } - } finally { - _lock.ExitReadLock(); - } - } - - /// - /// Tries to get the value for the . - /// - /// The key used to identify the object in the database. - /// - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValue(string key, JsonTypeInfo jsonTypeInfo, out T value) { - return TryGetValue(key, string.Empty, jsonTypeInfo, out value); - } - - - /// - /// Tries to get the value for the . - /// - /// The key used to identify the object in the database. - /// The encryption key used to decrypt the object if it is encrypted. - /// - /// The retrieved object of type T, or default if the object does not exist. - /// True if the value was found, otherwise false. - public bool TryGetValue(string key, string encryptionKey, JsonTypeInfo jsonTypeInfo, out T value) { - if (!TryGetValue(key, encryptionKey, out ReadOnlyMemory bytes)) { - value = default!; - return false; - } - value = JsonSerializer.Deserialize(bytes.Span, jsonTypeInfo)!; - return true; - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseRemovals.cs b/src/Sharpify.Data/DatabaseRemovals.cs deleted file mode 100644 index e660091..0000000 --- a/src/Sharpify.Data/DatabaseRemovals.cs +++ /dev/null @@ -1,118 +0,0 @@ -namespace Sharpify.Data; - -public sealed partial class Database { - /// - /// Removes the and its value from the inner dictionary. - /// - /// - /// True if the key was removed, false if it didn't exist or couldn't be removed. - public bool Remove(string key) { - try { - _lock.EnterWriteLock(); - if (!_data.Remove(key, out var val)) { - return false; - } - var estimatedSize = Helper.GetEstimatedSize(key, val); - Interlocked.Add(ref _estimatedSize, -estimatedSize); - Interlocked.Increment(ref _updatesCount); - if (Config.SerializeOnUpdate) { - Serialize(); - } - if (Config.TriggerUpdateEvents) { - InvokeDataEvent(new DataChangedEventArgs { - Key = key, - Value = val, - ChangeType = DataChangeType.Remove - }); - } - return true; - } finally { - _lock.ExitWriteLock(); - } - } - - /// - /// Removes all keys that match the . - /// - /// A predicate for the key - /// - /// - /// This method is thread-safe and will lock the database while removing the keys. - /// - /// - /// If TriggerUpdateEvents is enabled, this method will trigger a event for each key removed. - /// - /// - public void Remove(Func keySelector) => Remove(keySelector, null); - - /// - /// Removes all keys that match the . - /// - /// A predicate for the key - /// A prefix to be removed from the keys prior to the keySelector (mainly used for IDatabaseFilter implementations), leaving it as null will skip pre-filtering - /// - /// - /// This method is thread-safe and will lock the database while removing the keys. - /// - /// - /// If TriggerUpdateEvents is enabled, this method will trigger a event for each key removed. - /// - /// - public void Remove(Func keySelector, string? keyPrefix) { - try { - _lock.EnterWriteLock(); - - var predicate = keyPrefix is null - ? keySelector - : key => key.StartsWith(keyPrefix) && keySelector(key.Substring(keyPrefix.Length)); - - var matches = _data.Keys.Where(predicate); - - foreach (var key in matches) { - _data.Remove(key, out var val); - var estimatedSize = Helper.GetEstimatedSize(key, val); - Interlocked.Add(ref _estimatedSize, -estimatedSize); - Interlocked.Increment(ref _updatesCount); - - if (Config.TriggerUpdateEvents) { - InvokeDataEvent(new DataChangedEventArgs { - Key = key, - Value = val, - ChangeType = DataChangeType.Remove - }); - } - } - - if (Config.SerializeOnUpdate) { - Serialize(); - } - - } finally { - _lock.ExitWriteLock(); - } - } - - /// - /// Clears all keys and values from the database. - /// - public void Clear() { - try { - _lock.EnterWriteLock(); - _data.Clear(); - Interlocked.Exchange(ref _estimatedSize, 0); - Interlocked.Increment(ref _updatesCount); - if (Config.SerializeOnUpdate) { - Serialize(); - } - if (Config.TriggerUpdateEvents) { - InvokeDataEvent(new DataChangedEventArgs { - Key = "ALL", - Value = null, - ChangeType = DataChangeType.Remove - }); - } - } finally { - _lock.ExitWriteLock(); - } - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseSerialization.cs b/src/Sharpify.Data/DatabaseSerialization.cs deleted file mode 100644 index f2d36e0..0000000 --- a/src/Sharpify.Data/DatabaseSerialization.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Diagnostics; - -namespace Sharpify.Data; - -public sealed partial class Database { - private void EnsureUpsertsAreFinished() { - if (!Config.SerializeOnUpdate) { - while (_queue.TryDequeue(out var kvp)) { - _data[kvp.Key] = kvp.Value; - int estimatedSize = Helper.GetEstimatedSize(kvp); - Interlocked.Add(ref _estimatedSize, estimatedSize); - Interlocked.Increment(ref _updatesCount); - } - } - } - - /// - /// Checks if the database needs to be serialized. - /// - /// - private bool IsSerializationNecessary() { - if (_isInMemory) { - return false; - } - lock (_sLock) { - if (_updatesCount == _serializationReference) { - return false; - } - _serializationReference = _updatesCount; - return true; - } - } - - /// - /// Saves the database to the hard disk. - /// - public void Serialize() { - EnsureUpsertsAreFinished(); - - if (!IsSerializationNecessary()) { - return; - } - - Debug.Assert(!_isInMemory); - - int estimatedSize = GetOverestimatedSize(); - _serializer.Serialize(_data, estimatedSize); - } - - /// - /// Saves the database to the hard disk asynchronously. - /// - public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { - EnsureUpsertsAreFinished(); - - if (!IsSerializationNecessary()) { - return ValueTask.CompletedTask; - } - - Debug.Assert(!_isInMemory); - - return _serializer.SerializeAsync(_data, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/DatabaseUpserts.cs b/src/Sharpify.Data/DatabaseUpserts.cs deleted file mode 100644 index 8a148bb..0000000 --- a/src/Sharpify.Data/DatabaseUpserts.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -using MemoryPack; - -namespace Sharpify.Data; - -public sealed partial class Database { - /// - /// Updates or inserts a new @ . - /// - /// - /// - /// individual encryption key for this specific value - /// - /// - /// This pure method which accepts the value as ReadOnlySpan{byte} allows you to use more complex but also more efficient serializers. - /// - /// - public void Upsert(string key, ReadOnlySpan value, string encryptionKey = "") { - if (encryptionKey.Length is 0) { - _queue.Enqueue(new KeyValuePair(key, value.ToArray())); - } else { - byte[] encrypted = Helper.Instance.Encrypt(value, encryptionKey); - _queue.Enqueue(new KeyValuePair(key, encrypted)); - } - - if (Config.TriggerUpdateEvents) { - InvokeDataEvent(new DataChangedEventArgs { - Key = key, - Value = value.ToArray(), - ChangeType = DataChangeType.Upsert - }); - } - - EmptyQueue(); - } - - /// - /// Updates or inserts a new @ . - /// - /// - /// - /// individual encryption key for this specific value - /// - /// - /// This method directly inserts the array reference in to the database to reduce copying. - /// - /// - /// If you cannot ensure that this reference doesn't change, for example if using a pooled array, use the method instead. - /// - /// - public void Upsert(string key, byte[] value, string encryptionKey = "") { - if (encryptionKey.Length is 0) { - _queue.Enqueue(new KeyValuePair(key, value)); - } else { - ReadOnlySpan valueSpan = value; - byte[] encrypted = Helper.Instance.Encrypt(valueSpan, encryptionKey); - _queue.Enqueue(new KeyValuePair(key, encrypted)); - } - - if (Config.TriggerUpdateEvents) { - InvokeDataEvent(new DataChangedEventArgs { - Key = key, - Value = value, - ChangeType = DataChangeType.Upsert - }); - } - - EmptyQueue(); - } - - /// - /// Upserts a value into the database using the specified key. - /// - /// The type of the value being upserted. - /// The key used to identify the value. - /// The value to be upserted. - /// The encryption key used to encrypt the value. - /// a conditional check that the previously stored value must pass before being updated - /// - /// The upsert operation will either insert a new value if the key does not exist, - /// or update the existing value if the key already exists. - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// - /// False if the previous value exists, is not null, and the update condition is not met, otherwise True. - /// - public bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null) where T : IMemoryPackable { - if (updateCondition is not null) { - if (TryGetValue(key, encryptionKey, out var existingValue) && !updateCondition(existingValue)) { - return false; - } - } - byte[] bytes = MemoryPackSerializer.Serialize(value, _serializer.SerializerOptions); - Upsert(key, bytes, encryptionKey); - return true; - } - - /// - /// Upserts values into the database using the specified key. - /// - /// The type of the values being upserted. - /// The key used to identify the values. - /// The values to be upserted. - /// The encryption key used to encrypt the values. - /// a conditional check that the previously stored value must pass before being updated - /// - /// The upsert operation will either insert if the key does not exist, - /// or update the existing values if the key already exists. - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// - /// False if the previous values exist, is not null, and the update condition is not met, otherwise True. - /// - public bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null) where T : IMemoryPackable { - ArgumentNullException.ThrowIfNull(values, nameof(values)); - if (updateCondition is not null) { - if (TryGetValues(key, encryptionKey, out var existingValues) && !updateCondition(existingValues)) { - return false; - } - } - byte[] bytes = MemoryPackSerializer.Serialize(values, _serializer.SerializerOptions); - Upsert(key, bytes, encryptionKey); - return true; - } - - /// - /// Upserts values into the database using the specified key. - /// - /// The type of the values being upserted. - /// The key used to identify the values. - /// The values to be upserted. - /// The encryption key used to encrypt the values. - /// a conditional check that the previously stored value must pass before being updated - /// - /// The upsert operation will either insert if the key does not exist, - /// or update the existing values if the key already exists. - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// - /// False if the previous values exist, is not null, and the update condition is not met, otherwise True. - /// - public bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null) where T : IMemoryPackable { - return UpsertMany(key, values.ToArray(), encryptionKey, updateCondition); - } - - - /// - /// Updates or inserts a new @ . - /// - /// - /// - /// individual encryption key for this specific value - /// - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - public void Upsert(string key, string value, string encryptionKey = "") { - byte[] bytes = value.Length is 0 - ? Array.Empty() - : MemoryPackSerializer.Serialize(value, _serializer.SerializerOptions); - - Upsert(key, bytes, encryptionKey); - } - - /// - /// Updates or inserts a new @ . - /// - /// - /// - /// That can be used to serialize T - /// individual encryption key for this specific value - /// a conditional check that the previously stored value must pass before being updated - /// - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// - /// False if the previous value exists, is not null, and the update condition is not met, otherwise True. - /// - public bool Upsert(string key, T value, JsonTypeInfo jsonTypeInfo, string encryptionKey = "", Func? updateCondition = null) { - if (updateCondition is not null) { - if (!TryGetValue(key, encryptionKey, jsonTypeInfo, out var existingValue) || !updateCondition(existingValue)) { - return false; - } - } - byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(value, jsonTypeInfo); - Upsert(key, bytes, encryptionKey); - return true; - } - - // Adds items to the dictionary and serializes if needed at the end. - // This enables us to add multiple items at once without serializing multiple times. - // Essentially synchronizing concurrent writes. - // While the inner sequential addition to the dictionary makes it thread safe. - private void EmptyQueue() { - try { - _lock.EnterWriteLock(); - nint itemsAdded = 0; - while (_queue.TryDequeue(out var kvp)) { - _data[kvp.Key] = kvp.Value; - itemsAdded++; - int estimatedSize = Helper.GetEstimatedSize(kvp); - Interlocked.Add(ref _estimatedSize, estimatedSize); - Interlocked.Increment(ref _updatesCount); - } - if (itemsAdded is not 0 && Config.SerializeOnUpdate) { - Serialize(); - } - } finally { - _lock.ExitWriteLock(); - } - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/FlexibleDatabaseFilter{T}.cs b/src/Sharpify.Data/FlexibleDatabaseFilter{T}.cs deleted file mode 100644 index 7e2ece1..0000000 --- a/src/Sharpify.Data/FlexibleDatabaseFilter{T}.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Runtime.CompilerServices; - -using Sharpify.Collections; - -namespace Sharpify.Data; - -/// -/// Provides a light database filter by type. -/// -/// -/// Items that are upserted into the database using the filter, should not be retrieved without the filter as the key is modified. -/// -/// -public class FlexibleDatabaseFilter : IDatabaseFilter where T : IFilterable { - /// - /// The key filter, statically created for the type. - /// - public static readonly string KeyFilter = $"{typeof(T).Name}:"; - - /// - /// Creates a combined key (filter) for the specified key. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected string AcquireKey(ReadOnlySpan key) { - return string.Intern(KeyFilter.Concat(key)); - } - - /// - /// The database. - /// - protected readonly Database _database; - - /// - /// Creates a new database filter. - /// - /// - public FlexibleDatabaseFilter(Database database) { - _database = database; - } - - /// - public bool ContainsKey(string key) { - return _database.ContainsKey(AcquireKey(key)); - } - - - /// - public bool TryGetValue(string key, string encryptionKey, out T value) { - if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out var data)) { - value = default!; - return false; - } - value = T.Deserialize(data.Span)!; - return true; - } - - /// - public bool TryGetValues(string key, string encryptionKey, out T[] values) { - if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out ReadOnlyMemory data)) { - values = default!; - return false; - } - values = T.DeserializeMany(data.Span)!; - return true; - } - - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) { - if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out ReadOnlyMemory data)) { - return new RentedBufferWriter(0); - } - T[] values = T.DeserializeMany(data.Span)!; - var buffer = new RentedBufferWriter(values.Length + reservedCapacity); - buffer.WriteAndAdvance(values); - return buffer; - } - - /// - public bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null) { - if (updateCondition is not null) { - if (TryGetValue(key, encryptionKey, out var existingValue) && !updateCondition(existingValue)) { - return false; - } - } - var bytes = T.Serialize(value)!; - _database.Upsert(AcquireKey(key), bytes, encryptionKey); - return true; - } - - /// - public bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null) { - ArgumentNullException.ThrowIfNull(values, nameof(values)); - if (updateCondition is not null) { - if (TryGetValues(key, encryptionKey, out var existingValues) && !updateCondition(existingValues)) { - return false; - } - } - var bytes = T.SerializeMany(values)!; - _database.Upsert(AcquireKey(key), bytes, encryptionKey); - return true; - } - - /// - public bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null) { - return UpsertMany(key, values.ToArray(), encryptionKey, updateCondition); - } - - /// - public bool Remove(string key) { - return _database.Remove(AcquireKey(key)); - } - - - /// - public void Remove(Func keySelector) { - _database.Remove(keySelector, KeyFilter); - } - - - /// - public void Serialize() { - _database.Serialize(); - } - - - /// - public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { - return _database.SerializeAsync(cancellationToken); - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/GlobalSuppressions.cs b/src/Sharpify.Data/GlobalSuppressions.cs deleted file mode 100644 index 817119a..0000000 --- a/src/Sharpify.Data/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Trimming", "IL2091:Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations.", Justification = "See \"NativeAot Guide\" in the README.md", Scope = "member", Target = "~M:Sharpify.Data.Database.TryGetValue``1(System.String,System.String,``0@)~System.Boolean")] diff --git a/src/Sharpify.Data/Helper.cs b/src/Sharpify.Data/Helper.cs deleted file mode 100644 index 7c004ee..0000000 --- a/src/Sharpify.Data/Helper.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Buffers.Binary; -using System.Collections.Concurrent; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data; - -internal sealed class Helper : IDisposable { - internal static readonly Helper Instance = new(); - - private bool _disposed; - - private readonly ConcurrentDictionary _cachedProviders = new(Environment.ProcessorCount, 1); - - /// - /// Returns the encrypted version of the specified value using the specified key. - /// - /// - /// - public byte[] Encrypt(ReadOnlySpan value, string key) { - if (_cachedProviders.TryGetValue(key, out AesProvider? provider)) { - return provider!.EncryptBytes(value); - } - var newProvider = new AesProvider(key); - _cachedProviders.TryAdd(key, newProvider); - return newProvider.EncryptBytes(value); - } - - /// - /// Gets the encryptor for the specified key. - /// - /// - public ICryptoTransform GetEncryptor(string key) { - if (_cachedProviders.TryGetValue(key, out AesProvider? provider)) { - return provider!.CreateEncryptor(); - } - var newProvider = new AesProvider(key); - _cachedProviders.TryAdd(key, newProvider); - return newProvider.CreateEncryptor(); - } - - /// - /// Returns the decrypted version of the specified value using the specified key. - /// - /// - /// - public byte[] Decrypt(ReadOnlySpan value, string key) { - if (_cachedProviders.TryGetValue(key, out AesProvider? provider)) { - return provider!.DecryptBytes(value); - } - var newProvider = new AesProvider(key); - _cachedProviders.TryAdd(key, newProvider); - return newProvider.DecryptBytes(value, false); - } - - /// - /// Decrypts the specified value using the specified key and writes the result to the destination. - /// - /// - /// - /// - public int Decrypt(ReadOnlySpan value, Span destination, string key) { - if (_cachedProviders.TryGetValue(key, out AesProvider? provider)) { - return provider!.DecryptBytes(value, destination); - } - var newProvider = new AesProvider(key); - _cachedProviders.TryAdd(key, newProvider); - return newProvider.DecryptBytes(value, destination, false); - } - - /// - /// Gets the decryptor for the specified key. - /// - /// - /// - public ICryptoTransform GetDecryptor(string key) { - if (_cachedProviders.TryGetValue(key, out AesProvider? provider)) { - return provider!.CreateDecryptor(); - } - var newProvider = new AesProvider(key); - _cachedProviders.TryAdd(key, newProvider); - return newProvider.CreateDecryptor(); - } - - public void Dispose() { - if (_disposed) { - return; - } - foreach (var provider in _cachedProviders.Values) { - provider.Dispose(); - } - _disposed = true; - } - - /// - /// Returns the size of the serialized collection from the header - /// - /// - /// - /// Only use with MemoryPack - /// - public static int GetRequiredLength(ReadOnlySpan data) { - const int lengthSize = 4; // 4 bytes for the length - - if (data.Length < lengthSize) { - return 0; - } - - var length = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, lengthSize)); - length = length == -1 ? 0 : length; - - return Math.Min(length, data.Length); - } - - /// - /// Gets the size of the file. - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetFileSize(string path) { - var info = new FileInfo(path); - return unchecked((int)info.Length); - } - - /// - /// Gets the estimated size of the key-value pair. - /// - /// - public static int GetEstimatedSize(KeyValuePair kvp) - => GetEstimatedSize(kvp.Key, kvp.Value); - - - /// - /// Gets the estimated size of a key-value pair. - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetEstimatedSize(ReadOnlySpan key, ReadOnlySpan value) - => key.Length * sizeof(char) + value.Length; - - /// - /// Reads the data to the buffer - /// - /// - /// - /// - /// - public static void ReadToRenterBufferWriter(ref RentedBufferWriter buffer, ReadOnlySpan data, int length) { - var reader = new MemoryPackReader(data, NullOptionalState); - ref var arr = ref buffer.GetReferenceUnsafe(); - Span span = arr.AsSpan(0, length)!; - reader.ReadSpan(ref span); - buffer.Advance(length); - } - - /// - /// A null optional state for use with - /// - private static readonly MemoryPackReaderOptionalState NullOptionalState = MemoryPackReaderOptionalStatePool.Rent(null); -} \ No newline at end of file diff --git a/src/Sharpify.Data/IDatabaseFilter{T}.cs b/src/Sharpify.Data/IDatabaseFilter{T}.cs deleted file mode 100644 index 7c467e2..0000000 --- a/src/Sharpify.Data/IDatabaseFilter{T}.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Sharpify.Collections; - -namespace Sharpify.Data; - -/// -/// Represents a filter for a database that provides operations for querying, retrieving, and modifying data. -/// -/// The type of data stored in the database. -public interface IDatabaseFilter { - /// - /// Checks if the filtered database contains the specified key. - /// - /// The key to check. - /// true if the database contains the key; otherwise, false. - bool ContainsKey(string key); - - /// - /// Gets the value for the specified key from the database. - /// - /// The key to retrieve the value for. - /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. - /// true if the value was successfully retrieved; otherwise, false. - bool TryGetValue(string key, out T value) => TryGetValue(key, string.Empty, out value); - - /// - /// Gets the value for the specified key from the database using the specified encryption key. - /// - /// The key to retrieve the value for. - /// The encryption key to use. - /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. - /// true if the value was successfully retrieved; otherwise, false. - bool TryGetValue(string key, string encryptionKey, out T value); - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - RentedBufferWriter TryReadToRentedBuffer(string key, int reservedCapacity = 0) => TryReadToRentedBuffer(key, string.Empty, reservedCapacity); - - /// - /// Tries to get the values for the and write it to a - /// - /// - /// - /// Reserved capacity after the values, useful to write additional data - /// - /// A rented buffer writer containing the values if they were found, otherwise a disabled buffer writer (can be checked with ) - /// - RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0); - - /// - /// Gets the values for the specified key from the database. - /// - /// The key to retrieve the value for. - /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. - /// true if the value was successfully retrieved; otherwise, false. - bool TryGetValues(string key, out T[] values) => TryGetValues(key, string.Empty, out values); - - /// - /// Gets the values for the specified key from the database using the specified encryption key. - /// - /// The key to retrieve the value for. - /// The encryption key to use. - /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. - /// true if the value was successfully retrieved; otherwise, false. - bool TryGetValues(string key, string encryptionKey, out T[] values); - - /// - /// Upserts the value into the database. - /// - /// The key to upsert the value for. - /// The value to upsert. - /// The encryption key to use. - /// a conditional check that the previously stored value must pass before being updated - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// False if the previous value exists, is not null, and the update condition is not met, otherwise True. - /// - bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null); - - /// - /// Upserts multiple values into the database under a single key. - /// - /// The key to upsert the values for. - /// The values to upsert. - /// The encryption key to use. - /// a conditional check that the previously stored value must pass before being updated - /// - /// Null values are disallowed and will cause an exception to be thrown. - /// - /// - /// False if the previous values exist, is not null, and the update condition is not met, otherwise True. - /// - bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null); - - /// - /// Upserts multiple values into the database under a single key. - /// - /// The key to upsert the values for. - /// The values to upsert. - /// The encryption key to use. - /// a conditional check that the previously stored value must pass before being updated - /// - /// False if the previous values exist, is not null, and the update condition is not met, otherwise True. - /// - bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null); - - /// - /// Removes the item with the specified key from the filtered database. - /// - /// The key of the item to remove. - /// true if the item was successfully removed; otherwise, false. - bool Remove(string key); - - /// - /// Removes all keys that match the , while applying the key filtering - /// - /// - /// - /// - /// This method is thread-safe and will lock the database while removing the keys. - /// - /// - /// If TriggerUpdateEvents is enabled, this method will trigger a event for each key removed. - /// - /// - void Remove(Func keySelector); - - /// - /// Serializes the database. - /// - public void Serialize(); - - /// - /// Serializes the database asynchronously. - /// - public ValueTask SerializeAsync(CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/Sharpify.Data/IFilterable{T}.cs b/src/Sharpify.Data/IFilterable{T}.cs deleted file mode 100644 index 6e7b101..0000000 --- a/src/Sharpify.Data/IFilterable{T}.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Sharpify.Data; - -/// -/// Represents a filterable type. -/// -/// The type of the filterable object. -public interface IFilterable { - /// - /// Serializes the specified value into a byte array. - /// - /// - /// - static abstract byte[]? Serialize(T? value); - - /// - /// Serializes multiple values into a byte array. - /// - /// - /// - static abstract byte[]? SerializeMany(T[]? values); - - /// - /// Deserializes the specified data into a value. - /// - /// - /// - static abstract T? Deserialize(ReadOnlySpan data); - - /// - /// Deserializes the specified data into multiple values. - /// - /// - /// - static abstract T[]? DeserializeMany(ReadOnlySpan data); -} diff --git a/src/Sharpify.Data/MemoryPackDatabaseFilter{T}.cs b/src/Sharpify.Data/MemoryPackDatabaseFilter{T}.cs deleted file mode 100644 index 36ce80e..0000000 --- a/src/Sharpify.Data/MemoryPackDatabaseFilter{T}.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Runtime.CompilerServices; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data; - -/// -/// Provides a light database filter by type. -/// -/// -/// Items that are upserted into the database using the filter, should not be retrieved without the filter as the key is modified. -/// -/// -public class MemoryPackDatabaseFilter : IDatabaseFilter where T : IMemoryPackable { - /// - /// The key filter, statically created for the type. - /// - public static readonly string KeyFilter = $"{typeof(T).Name}:"; - - /// - /// Creates a combined key (filter) for the specified key. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected string AcquireKey(ReadOnlySpan key) { - return string.Intern(KeyFilter.Concat(key)); - } - - /// - /// The database. - /// - protected readonly Database _database; - - /// - /// Creates a new database filter. - /// - /// - public MemoryPackDatabaseFilter(Database database) { - _database = database; - } - - /// - public bool ContainsKey(string key) { - return _database.ContainsKey(AcquireKey(key)); - } - - /// - public bool TryGetValue(string key, string encryptionKey, out T value) { - return _database.TryGetValue(AcquireKey(key), encryptionKey, out value); - } - - /// - public bool TryGetValues(string key, string encryptionKey, out T[] values) { - return _database.TryGetValues(AcquireKey(key), encryptionKey, out values); - } - - /// - public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) { - return _database.TryReadToRentedBuffer(AcquireKey(key), encryptionKey, reservedCapacity); - } - - /// - public bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null) { - return _database.Upsert(AcquireKey(key), value, encryptionKey, updateCondition); - } - - /// - public bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null) { - return _database.UpsertMany(AcquireKey(key), values, encryptionKey, updateCondition); - } - - /// - public bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null) { - return _database.UpsertMany(AcquireKey(key), values, encryptionKey, updateCondition); - } - - /// - public bool Remove(string key) { - return _database.Remove(AcquireKey(key)); - } - - /// - public void Remove(Func keySelector) { - _database.Remove(keySelector, KeyFilter); - } - - /// - public void Serialize() { - _database.Serialize(); - } - - /// - public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { - return _database.SerializeAsync(cancellationToken); - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/README.md b/src/Sharpify.Data/README.md deleted file mode 100644 index f6a1ba8..0000000 --- a/src/Sharpify.Data/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Sharpify.Data - -An extension of `Sharpify` focused on data. - -## Features - -* `Database` is the base type for the data base, it is key-value-pair based local database - saved on disk. -* `IDatabaseFilter` is an interface which acts as an alternative to `DbContext` and provides enhanced type safety for contexts. -* `MemoryPackDatabaseFilter` is an implementation which focuses on types that implement `IMemoryPackable` from `MemoryPack`. -* `FlexibleDatabaseFilter` is an implementation focusing on types which need custom serialization logic. To use this, you type `T` will need to implement `IFilterable` which has methods for serialization and deserialization of single `T` and `T[]`. If you can choose to implement only one of the two. -* **Concurrency** - `Database` uses highly performant synchronous concurrency models and is completely thread-safe. -* **Disk Usage** - `Database` tracks inner changes and skips serialization if no changes occurred, enabling usage of periodic serialization without resource waste. -* **GC Optimization** - `Database` heavily uses pooling for encryption, decryption, type conversion, serialization and deserialization to minimize GC overhead, very rarely does it allocate single-use memory and only when absolutely necessary. -* **HotPath APIs** - `Database` is optimized for hot paths, as such it provides a number of APIs that specifically combine features for maximum performance and minimal GC overhead. Like the `TryReadToRentedBuffer` methods which is optimized for adding data to a table. -* **Runtime Optimization** - Upon initialization, `Database` chooses specific serializers and deserializers tailored for specific configurations, minimizing the amount of runnable code during runtime that would've been wasted on different checks. - -## Notes - -* Initialization of with regular and async factory methods, they will guide you for using the options of configuration. -* It is crucial to use the factory methods for database initialization, and **NOT** use activators or constructors, the factory methods select configuration specific abstractions that are optimized per the the type of database you want. -* The heart of the performance of these databases which use [MemoryPack](https://github.com/Cysharp/MemoryPack) for extreme performance binary serialization. -* `Database` has upsert overloads which support any `IMemoryPackable` from [MemoryPack](https://github.com/Cysharp/MemoryPack). -* Both `Database` implements `IDisposable` and should be disposed after usage to make sure all resources are released, this should also prevent possible issues if the object is removed from memory while an operation is ongoing (i.e the user closes the application when a write isn't finished) -* The database is key-value-pair based, and operation on each key have O(1) complexity, serialization scales rather linearly (No way around it). -* For very large datasets, there might be more suitable databases, but if you still want to use this, you could enable `[gcAllowVeryLargeObjects](https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element), as per the Microsoft docs, on 64 bit system it should allow the object to be larger than 2GB, which is normally the limit. -* To ensure integrity data copies are kept to a minimum, and allocations are designed to happen only when required to ensure data integrity (i.e to ensure the database stores real data, and to ensure the actual data is not exposed to the outside), the database uses pooling for any disposable memory operations to ensure minimal GC overhead. - -## NativeAot Guide - -As of writing this, `MemoryPack`'s NativeAot support is broken, for any type that isn't already in their cached types, the `MemoryPackFormatterProvider` uses reflection to get the formatter (that includes types decorated with `MemoryPackable` which in turn implement `IMemoryPackable`), which fails in NativeAot. -As a workaround, we need to add the formatters ourselves, to do this, take any 1 static entry point, that activates before the database is loaded, and add this: - -```csharp -// for every T type that relies on MemoryPack for serialization, and their inheritance hierarchy -// This includes types that implement IMemoryPackable (i.e types that are decorated with MemoryPackable) -MemoryPackFormatterProvider.Register(); -// If the type is a collection or dictionary use the other corresponding overloads: -MemoryPackFormatterProvider.RegisterCollection(); -// or -MemoryPackFormatterProvider.RegisterDictionary(); -// and so on... -// for all overloads check peek the definition of MemoryPackFormatterProvider, or their Github Repo -``` - -**Note:** Make sure you don't create a new static constructor in those types, `MemoryPack` already creates those, you will need to find a different entry point. - -With this the serializer should be able to bypass the part using reflection, and thus work even on NativeAot. - -P.S. The base type of the Database is already registered the same way on its own static constructor. - -## Contact - -For bug reports, feature requests or offers of support/sponsorship contact diff --git a/src/Sharpify.Data/Serializers/AbstractSerializer.cs b/src/Sharpify.Data/Serializers/AbstractSerializer.cs deleted file mode 100644 index 283971b..0000000 --- a/src/Sharpify.Data/Serializers/AbstractSerializer.cs +++ /dev/null @@ -1,66 +0,0 @@ -using MemoryPack; - -namespace Sharpify.Data.Serializers; - -/// -/// Provides an abstraction for creating a readonly serializer -/// -internal abstract class AbstractSerializer { - protected readonly string _path; - internal readonly MemoryPackSerializerOptions SerializerOptions; - - protected AbstractSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) { - _path = path; - SerializerOptions = encoding switch { - StringEncoding.Utf8 => MemoryPackSerializerOptions.Utf8, - StringEncoding.Utf16 => MemoryPackSerializerOptions.Utf16, - _ => MemoryPackSerializerOptions.Default - }; - } - - /// - /// Serializes the given dictionary - /// - /// - /// - internal abstract void Serialize(Dictionary dict, int estimatedSize); - - /// - /// Serializes the given dictionary asynchronously - /// - /// - /// - /// - internal abstract ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default); - - /// - /// Deserializes the path to a dictionary - /// - /// - internal abstract Dictionary Deserialize(int estimatedSize); - - /// - /// Deserializes the path to a dictionary asynchronously - /// - /// - /// - internal abstract ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default); - - /// - /// Creates a serializer based on the given configuration - /// - /// - /// - /// - internal static AbstractSerializer Create(DatabaseConfiguration configuration) { - return configuration switch { - { Path: "", IgnoreCase: false } => new DisabledSerializer(configuration.Path, configuration.Encoding), - { Path: "", IgnoreCase: true } => new DisabledIgnoreCaseSerializer(configuration.Path, configuration.Encoding), - { HasEncryption: true, IgnoreCase: true } => new IgnoreCaseEncryptedSerializer(configuration.Path, configuration.EncryptionKey), - { HasEncryption: true, IgnoreCase: false } => new EncryptedSerializer(configuration.Path, configuration.EncryptionKey), - { HasEncryption: false, IgnoreCase: true } => new IgnoreCaseSerializer(configuration.Path), - { HasEncryption: false, IgnoreCase: false } => new Serializer(configuration.Path), - _ => throw new ArgumentException("Invalid configuration") - }; - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/Serializers/DisabledSerializers.cs b/src/Sharpify.Data/Serializers/DisabledSerializers.cs deleted file mode 100644 index 87182d3..0000000 --- a/src/Sharpify.Data/Serializers/DisabledSerializers.cs +++ /dev/null @@ -1,37 +0,0 @@ -using MemoryPack; - -namespace Sharpify.Data.Serializers; - -/// -/// A serializer for a database without encryption and case sensitive keys -/// -internal class DisabledSerializer : AbstractSerializer { - internal DisabledSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { - } - - /// - internal override Dictionary Deserialize(int estimatedSize) => new(); - - /// - internal override ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) => ValueTask.FromResult(new Dictionary()); - - /// - internal override void Serialize(Dictionary dict, int estimatedSize) { } - -/// - internal override ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; -} - -/// -/// A serializer for a database without encryption and case sensitive keys -/// -internal class DisabledIgnoreCaseSerializer : DisabledSerializer { - internal DisabledIgnoreCaseSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { - } - - /// - internal override Dictionary Deserialize(int estimatedSize) => new(StringComparer.OrdinalIgnoreCase); - - /// - internal override ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) => ValueTask.FromResult(new Dictionary(StringComparer.OrdinalIgnoreCase)); -} \ No newline at end of file diff --git a/src/Sharpify.Data/Serializers/EncryptedSerializer.cs b/src/Sharpify.Data/Serializers/EncryptedSerializer.cs deleted file mode 100644 index 0b81d50..0000000 --- a/src/Sharpify.Data/Serializers/EncryptedSerializer.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Security.Cryptography; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data.Serializers; - -/// -/// A serializer for a database encryption and case sensitive keys -/// -internal class EncryptedSerializer : AbstractSerializer { - protected readonly string _key; - - internal EncryptedSerializer(string path, string key, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { - _key = key; - } - -/// - internal override Dictionary Deserialize(int estimatedSize) { - if (estimatedSize is 0) { - return new Dictionary(); - } - - using var rawBuffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - int rawRead = file.Read(rawBuffer.GetSpan()); - rawBuffer.Advance(rawRead); - ReadOnlySpan rawSpan = rawBuffer.WrittenSpan; - using var decryptedBuffer = new RentedBufferWriter(rawSpan.Length); - int decryptedRead = Helper.Instance.Decrypt(rawSpan, decryptedBuffer.GetSpan(), _key); - decryptedBuffer.Advance(decryptedRead); - ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; - var dict = MemoryPackSerializer.Deserialize>(decrypted, SerializerOptions); - return dict ?? new Dictionary(); - } - -/// - internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { - if (estimatedSize is 0) { - return new Dictionary(); - } - using var file = new FileStream(_path, FileMode.Open); - using var transform = Helper.Instance.GetDecryptor(_key); - using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Read); - var dict = await MemoryPackSerializer.DeserializeAsync>(cryptoStream, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - return dict ?? new Dictionary(); - } - -/// - internal override void Serialize(Dictionary dict, int estimatedSize) { - using var buffer = new RentedBufferWriter(estimatedSize + AesProvider.ReservedBufferSize); - MemoryPackSerializer.Serialize(buffer, dict, SerializerOptions); - using var file = new FileStream(_path, FileMode.Create); - using ICryptoTransform transform = Helper.Instance.GetEncryptor(_key); - using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Write); - cryptoStream.Write(buffer.WrittenSpan); - } - -/// - internal override async ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) { - using var file = new FileStream(_path, FileMode.Create); - using ICryptoTransform transform = Helper.Instance.GetEncryptor(_key); - using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Write); - await MemoryPackSerializer.SerializeAsync(cryptoStream, dict, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/Serializers/IgnoreCaseEncryptedSerializer.cs b/src/Sharpify.Data/Serializers/IgnoreCaseEncryptedSerializer.cs deleted file mode 100644 index 3458c25..0000000 --- a/src/Sharpify.Data/Serializers/IgnoreCaseEncryptedSerializer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Security.Cryptography; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data.Serializers; - -/// -/// A serializer for a database encryption and case-sensitive keys -/// -internal class IgnoreCaseEncryptedSerializer : EncryptedSerializer { - internal IgnoreCaseEncryptedSerializer(string path, string key, StringEncoding encoding = StringEncoding.Utf8) : base(path, key, encoding) { - } - -/// - internal override Dictionary Deserialize(int estimatedSize) { - if (estimatedSize is 0) { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - using var rawBuffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - int rawRead = file.Read(rawBuffer.GetSpan()); - rawBuffer.Advance(rawRead); - ReadOnlySpan rawSpan = rawBuffer.WrittenSpan; - using var decryptedBuffer = new RentedBufferWriter(rawSpan.Length); - int decryptedRead = Helper.Instance.Decrypt(rawSpan, decryptedBuffer.GetSpan(), _key); - decryptedBuffer.Advance(decryptedRead); - ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; - Dictionary dict = IgnoreCaseSerializer.FromSpan(decrypted); - return dict; - } - -/// - internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { - if (estimatedSize is 0) { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - using var buffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - using ICryptoTransform transform = Helper.Instance.GetDecryptor(_key); - using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Read); - int numRead = await cryptoStream.ReadAsync(buffer.GetMemory(), cancellationToken).ConfigureAwait(false); - buffer.Advance(numRead); - Dictionary dict = IgnoreCaseSerializer.FromSpan(buffer.WrittenMemory); - return dict; - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/Serializers/IgnoreCaseSerializer.cs b/src/Sharpify.Data/Serializers/IgnoreCaseSerializer.cs deleted file mode 100644 index 23cea88..0000000 --- a/src/Sharpify.Data/Serializers/IgnoreCaseSerializer.cs +++ /dev/null @@ -1,57 +0,0 @@ -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data.Serializers; - -/// -/// A serializer for a database without encryption and case sensitive keys -/// -internal class IgnoreCaseSerializer : Serializer { - internal IgnoreCaseSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { - } - - internal static Dictionary FromSpan(ReadOnlyMemory bin) { - ReadOnlySpan data = bin.Span; - return FromSpan(data); - } - - internal static Dictionary FromSpan(ReadOnlySpan bin) { - if (bin.Length is 0) { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - var formatter = new OrdinalIgnoreCaseStringDictionaryFormatter(); - var state = MemoryPackReaderOptionalStatePool.Rent(MemoryPackSerializerOptions.Default); - var reader = new MemoryPackReader(bin, state); - Dictionary? dict = null; - formatter.GetFormatter().Deserialize(ref reader, ref dict); - return dict ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - /// - internal override Dictionary Deserialize(int estimatedSize) { - if (estimatedSize is 0) { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - using var buffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - int numRead = file.Read(buffer.Buffer, 0, estimatedSize); - buffer.Advance(numRead); - ReadOnlySpan deserialized = buffer.WrittenSpan; - Dictionary dict = FromSpan(deserialized); - return dict; - } - - /// - internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { - if (estimatedSize is 0) { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - using var buffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - int numRead = await file.ReadAsync(buffer.GetMemory(), cancellationToken).ConfigureAwait(false); - buffer.Advance(numRead); - Dictionary dict = FromSpan(buffer.WrittenMemory); - return dict; - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/Serializers/Serializer.cs b/src/Sharpify.Data/Serializers/Serializer.cs deleted file mode 100644 index 1c98f88..0000000 --- a/src/Sharpify.Data/Serializers/Serializer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data.Serializers; - -/// -/// A serializer for a database without encryption and case sensitive keys -/// -internal class Serializer : AbstractSerializer { - internal Serializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { - } - -/// - internal override Dictionary Deserialize(int estimatedSize) { - if (estimatedSize is 0) { - return new Dictionary(); - } - using var buffer = new RentedBufferWriter(estimatedSize); - using var file = new FileStream(_path, FileMode.Open); - int numRead = file.Read(buffer.Buffer, 0, estimatedSize); - buffer.Advance(numRead); - Dictionary dict = - MemoryPackSerializer.Deserialize>(buffer.WrittenSpan, SerializerOptions) - ?? new Dictionary(); - return dict; - } - -/// - internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { - if (estimatedSize is 0) { - return new Dictionary(); - } - using var file = new FileStream(_path, FileMode.Open); - var dict = await MemoryPackSerializer.DeserializeAsync>(file, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - return dict ?? new Dictionary(); - } - -/// - internal override void Serialize(Dictionary dict, int estimatedSize) { - using var file = new FileStream(_path, FileMode.Create); - using var buffer = new RentedBufferWriter(estimatedSize); - MemoryPackSerializer.Serialize(in buffer, in dict, SerializerOptions); - file.Write(buffer.WrittenSpan); - } - -/// - internal override async ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) { - using var file = new FileStream(_path, FileMode.Create); - await MemoryPackSerializer.SerializeAsync(file, dict, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Sharpify.Data/Sharpify.Data.csproj b/src/Sharpify.Data/Sharpify.Data.csproj deleted file mode 100644 index 27e0c62..0000000 --- a/src/Sharpify.Data/Sharpify.Data.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net9.0;net8.0 - enable - 2.6.0 - enable - true - David Shnayder - David Shnayder - MIT - CHANGELOGLATEST.md - True - Sharpify.Data - An extension of Sharpify, focused on Data - https://github.com/dusrdev/Sharpify - https://github.com/dusrdev/Sharpify - git - Extensions;HighPerformance;Data;Database - true - true - false - true - true - - - - - - - - - - - - - - - - - <_Parameter1>Sharpify.Data.Tests - - - - \ No newline at end of file diff --git a/src/Sharpify/AesProvider.cs b/src/Sharpify/AesProvider.cs index aa388f1..c5c9e97 100644 --- a/src/Sharpify/AesProvider.cs +++ b/src/Sharpify/AesProvider.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -41,9 +42,9 @@ public AesProvider(string strKey) { // Creates a usable fixed length key from the string password private static byte[] CreateKey(ReadOnlySpan strKey) { - using var buffer = new RentedBufferWriter(strKey.Length * sizeof(char)); - _ = Encoding.UTF8.GetBytes(strKey, buffer); - return SHA256.HashData(buffer.WrittenSpan); + using var owner = ArrayPool.Shared.Rent(strKey.Length * sizeof(char), out Span span); + int written = Encoding.UTF8.GetBytes(strKey, span); + return SHA256.HashData(span.Slice(0, written)); } @@ -57,9 +58,8 @@ public static string GeneratePassword(string password, int iterations = 991) { //generate a random salt for hashing //hash password given salt and iterations (default to 1000) //iterations provide difficulty when cracking - using var pbkdf2 = new Rfc2898DeriveBytes(password, SaltSize, iterations, HashAlgorithmName.SHA512); - var hash = pbkdf2.GetBytes(SaltSize); - var salt = pbkdf2.Salt; + byte[] salt = RandomNumberGenerator.GetBytes(SaltSize); + var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA512, SaltSize); // create format for hash text // salt|iterations|hash @@ -90,7 +90,7 @@ public static bool IsPasswordValid(string password, string hashedPassword) { hpSpan.Split(parts, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); ReadOnlySpan origSalt = Convert.FromBase64String(hashedPassword[parts[0]]); - hpSpan[parts[1]].TryConvertToInt32(out var origIterations); + var origIterations = int.Parse(hpSpan[parts[1]], NumberStyles.Integer, CultureInfo.CurrentCulture); ReadOnlySpan origHash = hashedPassword[parts[2]]; //generate hash from test password and original salt and iterations @@ -107,13 +107,13 @@ public static bool IsPasswordValid(string password, string hashedPassword) { /// original text /// Unicode string public string Encrypt(ReadOnlySpan unencrypted) { - using var bytesBuffer = new RentedBufferWriter(unencrypted.Length * sizeof(char)); - _ = Encoding.UTF8.GetBytes(unencrypted, bytesBuffer); // IBufferWriter overload advances automatically - var writtenSpan = bytesBuffer.WrittenSpan; - using var encryptedBuffer = new RentedBufferWriter(writtenSpan.Length + ReservedBufferSize); - int encryptedWritten = EncryptBytes(writtenSpan, encryptedBuffer.GetSpan()); - encryptedBuffer.Advance(encryptedWritten); - return Convert.ToBase64String(encryptedBuffer.WrittenSpan); + using var bytesOwner = ArrayPool.Shared.Rent(unencrypted.Length * sizeof(char), out Span bytesBuffer); + int written = Encoding.UTF8.GetBytes(unencrypted, bytesBuffer); + var writtenSpan = bytesBuffer.Slice(0, written); + + using var encryptedOwner = ArrayPool.Shared.Rent(writtenSpan.Length + ReservedBufferSize, out Span encryptedBuffer); + written = EncryptBytes(writtenSpan, encryptedBuffer); + return Convert.ToBase64String(encryptedBuffer.Slice(0, written)); } /// @@ -122,10 +122,9 @@ public string Encrypt(ReadOnlySpan unencrypted) { /// Returns an empty string if it fails public string Decrypt(string encrypted) { var buffer = Convert.FromBase64String(encrypted); - using var decryptedBuffer = new RentedBufferWriter(buffer.Length); - int decryptedWritten = DecryptBytes(buffer, decryptedBuffer.GetSpan()); - decryptedBuffer.Advance(decryptedWritten); - ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; + using var decryptedOwner = ArrayPool.Shared.Rent(buffer.Length, out Span span); + int written = DecryptBytes(buffer, span); + ReadOnlySpan decrypted = span.Slice(0, written); return decrypted.Length is 0 ? string.Empty : Encoding.UTF8.GetString(decrypted); @@ -211,13 +210,12 @@ public int DecryptBytes(ReadOnlySpan encrypted, Span destination, bo /// original url /// Encrypted url with Base64Url encoding public string EncryptUrl(string url) { - using var buffer = new RentedBufferWriter(url.Length * sizeof(char)); - _ = Encoding.UTF8.GetBytes(url, buffer); // IBufferWriter overload advances automatically - ReadOnlySpan bytesSpan = buffer.WrittenSpan; - using var encryptedBuffer = new RentedBufferWriter(bytesSpan.Length + ReservedBufferSize); - int encryptedWritten = EncryptBytes(bytesSpan, encryptedBuffer.GetSpan()); - encryptedBuffer.Advance(encryptedWritten); - return Base64UrlEncode(encryptedBuffer.WrittenSpan); + using var bufferOwner = ArrayPool.Shared.Rent(url.Length * sizeof(char), out Span buffer); + int written = Encoding.UTF8.GetBytes(url, buffer); // IBufferWriter overload advances automatically + ReadOnlySpan bytesSpan = buffer.Slice(0, written); + using var encryptedOwner = ArrayPool.Shared.Rent(bytesSpan.Length + ReservedBufferSize, out Span encrypted); + written = EncryptBytes(bytesSpan, encrypted); + return Base64UrlEncode(encrypted.Slice(0, written)); } /// @@ -228,10 +226,9 @@ public string EncryptUrl(string url) { /// Returns an empty string if it fails public string DecryptUrl(string encryptedUrl) { var base64 = Base64UrlDecode(encryptedUrl); - using var decryptedBuffer = new RentedBufferWriter(base64.Length); - int decryptedWritten = DecryptBytes(base64, decryptedBuffer.GetSpan()); - decryptedBuffer.Advance(decryptedWritten); - ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; + using var decryptedOwner = ArrayPool.Shared.Rent(base64.Length, out Span buffer); + int written = DecryptBytes(base64, buffer); + ReadOnlySpan decrypted = buffer.Slice(0, written); return decrypted.Length is 0 ? string.Empty : Encoding.UTF8.GetString(decrypted); @@ -284,4 +281,4 @@ public void Dispose() { _disposed = true; GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/Sharpify/Build.txt b/src/Sharpify/Build.txt deleted file mode 100644 index b25b581..0000000 --- a/src/Sharpify/Build.txt +++ /dev/null @@ -1,10 +0,0 @@ -nuget: -dotnet clean -c Release -dotnet build -c Release -dotnet pack -c Release -p:SignAssembly="" - -dll: -dotnet build -c Release -p:SignAssembly="" - -docs: -git subtree push --prefix docs https://github.com/dusrdev/Sharpify.wiki.git master diff --git a/src/Sharpify/CHANGELOGLATEST.md b/src/Sharpify/CHANGELOGLATEST.md deleted file mode 100644 index 308d1f7..0000000 --- a/src/Sharpify/CHANGELOGLATEST.md +++ /dev/null @@ -1,8 +0,0 @@ -# CHANGELOG - -## v2.5.0 - -* Updated to support .NET 9.0 and optimized certain methods to use .NET 9 specific API's wherever possible. -* Added `BufferWrapper` which can be used to append items to a `Span` without managing indexes and capacity. This buffer also implement `IBufferWriter`, and as a `ref struct implementing an interface` it is only available on .NET 9.0 and above. -* `Utils.String.FormatBytes` now uses a much larger buffer size of 512 chars by default, to handle the edge case of `double.MaxValue` which would previously cause an `ArgumentOutOfRangeException` to be thrown or similarly any number of bytes that would be bigger than 1024 petabytes. The result will now also include thousands separators to improve readability. - * The inner implementation that uses this buffer size is pooled so this should not have any impact on performance. diff --git a/src/Sharpify/CollectionExtensions.cs b/src/Sharpify/CollectionExtensions.cs index 9c056b7..5cdff88 100644 --- a/src/Sharpify/CollectionExtensions.cs +++ b/src/Sharpify/CollectionExtensions.cs @@ -7,17 +7,10 @@ namespace Sharpify; public static partial class Extensions { - /// - /// Determines whether the specified collection is null or empty. - /// - /// The type of elements in the collection. - /// The collection to check. - /// true if the collection is null or empty; otherwise, false. - public static bool IsNullOrEmpty(this ICollection? collection) => collection is null or { Count: 0 }; - /// /// Returns the span of a list /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span AsSpan(this List list) => CollectionsMarshal.AsSpan(list); /// @@ -29,6 +22,7 @@ public static partial class Extensions { /// Items should not be added or removed from the while the ref is in use. /// The ref null can be detected using Unsafe.IsNullRef{T}(ref readonly T)" /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TValue GetValueRefOrNullRef( this Dictionary dictionary, TKey key) where TKey : notnull { @@ -44,6 +38,7 @@ public static ref TValue GetValueRefOrNullRef( /// /// Items should not be added to or removed from the while the ref is in use. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TValue? GetValueRefOrAddDefault( this Dictionary dictionary, TKey key, @@ -68,45 +63,6 @@ public static void CopyTo(this Dictionary dict, KeyV collection.CopyTo(arr, index); } - /// - /// Rents a buffer and copies the contents of the dictionary into it. - /// - /// The type of the dictionary keys. - /// The type of the dictionary values. - /// The dictionary to rent the buffer for. - /// A tuple containing the rented buffer as an array and an array segment representing the copied items. - /// - /// The array segment is required since the ArrayPool can return a buffer larger than the length of the dictionary, for any operations use the array segment - /// The array is returned as the reference for the buffer, and should be used to return the buffer to the array pool after use. You can use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (KeyValuePair[] rentedBuffer, ArraySegment> entries) RentBufferAndCopyEntries(this Dictionary dict) where TKey : notnull { - var count = dict.Count; - var arr = ArrayPool>.Shared.Rent(count); - dict.CopyTo(arr, 0); - var segment = new ArraySegment>(arr, 0, count); - return (arr, segment); - } - - /// - /// Returns a rented buffer to the shared . - /// - /// The type of elements in the array. - /// The array to return. - /// If used on a buffer that wasn't part of the shared array pool - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnBufferToSharedArrayPool(this T[] arr) => ArrayPool.Shared.Return(arr); - - /// - /// Returns a rented buffer to the . - /// - /// The type of elements in the array. - /// The array to return. - /// The array pool to return the buffer to. - /// If used on a buffer that wasn't part of the array pool - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnBufferToArrayPool(this T[] arr, ArrayPool pool) => pool.Return(arr); - /// /// Returns a new array with the elements sorted using the default comparer for the element type. /// diff --git a/src/Sharpify/Collections/BufferWrapper{T}.cs b/src/Sharpify/Collections/BufferWrapper{T}.cs index f3c582d..930eaf0 100644 --- a/src/Sharpify/Collections/BufferWrapper{T}.cs +++ b/src/Sharpify/Collections/BufferWrapper{T}.cs @@ -2,7 +2,6 @@ namespace Sharpify.Collections; -#if NET9_0_OR_GREATER /// /// Represents a buffer than be used to efficiently append items to a span. /// @@ -12,7 +11,11 @@ namespace Sharpify.Collections; /// /// The total length of the buffer. /// +#pragma warning disable CA1051 // Do not declare visible instance fields + public readonly int Length; +#pragma warning restore CA1051 // Do not declare visible instance fields + /// /// The current position of the buffer. @@ -22,7 +25,11 @@ namespace Sharpify.Collections; /// /// Initializes a string buffer that uses a pre-allocated buffer (potentially from the stack). /// +#pragma warning disable CA1000 // Do not declare static members on generic types + public static BufferWrapper Create(Span buffer) => new(buffer); +#pragma warning restore CA1000 // Do not declare static members on generic types + /// /// Represents a mutable interface over a buffer allocated in memory. @@ -65,10 +72,10 @@ public void Append(ReadOnlySpan items) { public void Advance(int count) => Position += count; /// - public Memory GetMemory(int sizeHint = 0) => throw new NotSupportedException("BufferWrapper does not support GetMemory"); + public readonly Memory GetMemory(int sizeHint = 0) => throw new NotSupportedException("BufferWrapper does not support GetMemory"); /// - public Span GetSpan(int sizeHint = 0) => _buffer.Slice(Position); + public readonly Span GetSpan(int sizeHint = 0) => _buffer.Slice(Position); /// /// Returns the character at the specified index. @@ -80,5 +87,4 @@ public void Append(ReadOnlySpan items) { /// Returns the used portion of the buffer as a readonly span. /// public readonly ReadOnlySpan WrittenSpan => _buffer.Slice(0, Position); -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Sharpify/Collections/LazyLocalPersistentDictionary.cs b/src/Sharpify/Collections/LazyLocalPersistentDictionary.cs deleted file mode 100644 index bc1e4cf..0000000 --- a/src/Sharpify/Collections/LazyLocalPersistentDictionary.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Text.Json; - -namespace Sharpify.Collections; - -/// -/// Represents a dictionary that persists its data to a local file but not in memory. -/// -public class LazyLocalPersistentDictionary : PersistentDictionary { - private readonly string _path; - private readonly StringComparer _stringComparer; - private static readonly Dictionary Empty = []; - - /// - /// Creates a new instance of with the and specified. - /// - /// The full path to the file to persist the dictionary to. - /// The comparer to use for the dictionary. - public LazyLocalPersistentDictionary(string path, StringComparer comparer) { - _path = path; - _stringComparer = comparer; - } - - /// - /// Creates a new instance of with the and . - /// - /// The path to the file to persist the dictionary to. - public LazyLocalPersistentDictionary(string path) : this(path, StringComparer.Ordinal) { } - - /// - /// Retrieves the value associated with the specified key from the persistent dictionary. - /// - /// The key of the value to retrieve. - /// - /// The value associated with the specified key if it exists in the dictionary; otherwise, null. - /// - protected override string? GetValueByKey(string key) { - if (!File.Exists(_path)) { - return null; - } - var length = checked((int)new FileInfo(_path).Length); - if (length is 0) { - return null; - } - using var buffer = new RentedBufferWriter(length); - using var file = File.Open(_path, FileMode.Open); - var numRead = file.Read(buffer.GetSpan()); - buffer.Advance(numRead); - ReadOnlySpan jsonUtf8Bytes = buffer.WrittenSpan; - var reader = new Utf8JsonReader(jsonUtf8Bytes, InternalHelper.JsonReaderOptions); - while (reader.Read()) { - if (reader.TokenType is not JsonTokenType.PropertyName) { - continue; - } - var property = reader.GetString(); - if (!_stringComparer.Equals(property, key)) { - _ = reader.TrySkip(); - continue; - } - reader.Read(); - var value = reader.GetString(); - return value; - } - return null; - } - - /// - /// Sets the key and value in the dictionary. - /// If the dictionary file does not exist, a new dictionary is created and the key-value pair is added. - /// If the dictionary file exists, the dictionary is deserialized and the key-value pair is added or updated. - /// - /// The key to set. - /// The value to set. - protected override void SetKeyAndValue(string key, string value) { - if (!File.Exists(_path)) { - _dict ??= new Dictionary(_stringComparer); - _dict[key] = value; - return; - } - var sDict = Deserialize(); - if (sDict is null) { - _dict ??= new Dictionary(_stringComparer); - _dict[key] = value; - return; - } - _dict = sDict; - _dict[key] = value; - } - - /// - protected override Dictionary? Deserialize() { - using var file = File.Open(_path, FileMode.Open); - return JsonSerializer.Deserialize(file, JsonContext.Default.DictionaryStringString); - } - - /// - protected override async Task SerializeAsync() { - await using var file = File.Open(_path, FileMode.Create); - await JsonSerializer.SerializeAsync(file, _dict, JsonContext.Default.DictionaryStringString).ConfigureAwait(false); - _dict = Empty; - } -} \ No newline at end of file diff --git a/src/Sharpify/Collections/LocalPersistentDictionary.cs b/src/Sharpify/Collections/LocalPersistentDictionary.cs deleted file mode 100644 index 8177462..0000000 --- a/src/Sharpify/Collections/LocalPersistentDictionary.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json; - -namespace Sharpify.Collections; - -/// -/// Represents a dictionary that persists its data to a local file. -/// -public class LocalPersistentDictionary : PersistentDictionary { - private readonly string _path; - - /// - /// Creates a new instance of with the and specified. - /// - /// The full path to the file to persist the dictionary to. - /// The comparer to use for the dictionary. - public LocalPersistentDictionary(string path, StringComparer comparer) { - _path = path; - if (!File.Exists(_path)) { - _dict = new Dictionary(comparer); - return; - } - var sDict = Deserialize(); - if (sDict is null) { - _dict = new Dictionary(comparer); - return; - } - _dict = new Dictionary(sDict, comparer); - } - - /// - /// Creates a new instance of with the and . - /// - /// The path to the file to persist the dictionary to. - public LocalPersistentDictionary(string path) : this(path, StringComparer.Ordinal) { } - - /// - protected override Dictionary? Deserialize() { - using var file = File.Open(_path, FileMode.Open); - return JsonSerializer.Deserialize(file, JsonContext.Default.DictionaryStringString); - } - - /// - protected override async Task SerializeAsync() { - await using var file = File.Open(_path, FileMode.Create); - await JsonSerializer.SerializeAsync(file, _dict, JsonContext.Default.DictionaryStringString).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Sharpify/Collections/PersistentDictionary.cs b/src/Sharpify/Collections/PersistentDictionary.cs deleted file mode 100644 index 42e753f..0000000 --- a/src/Sharpify/Collections/PersistentDictionary.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Collections.Concurrent; -using System.Globalization; -using System.Runtime.CompilerServices; - -namespace Sharpify.Collections; - -/// -/// Provides a thread-safe dictionary that can be efficiently persisted. -/// -public abstract class PersistentDictionary : IDisposable{ - /// - /// A thread-safe dictionary that stores string keys and values. - /// - protected Dictionary _dict = []; - - private readonly ConcurrentQueue> _queue = new(); - - private readonly SemaphoreSlim _semaphore = new(1, 1); - - private volatile bool _disposed; - - /// - /// Gets the number of key-value pairs contained in the PersistentDictionary. - /// - public int Count => _dict.Count; - - /// - /// Gets the value associated with the specified key. - /// - /// The key to retrieve the value for. - /// The value associated with the specified key, or null if the key is not found. - protected virtual string? GetValueByKey(string key) { - if (Count is 0) { - return null; - } - ref var value = ref _dict.GetValueRefOrNullRef(key); - return Unsafe.IsNullRef(ref value) ? null : value; - } - - /// - /// Gets the value associated with the specified key. - /// - public virtual string? this[string key] => GetValueByKey(key); - - /// - /// Gets the value associated with the specified key, or creates a new key-value pair if the key does not exist. - /// - /// The key of the element to get or create. - /// The default value to use if the key does not exist. - /// The value associated with the specified key, or the default value if the key does not exist. - public virtual async ValueTask GetOrCreateAsync(string key, string @default) { - if (string.IsNullOrWhiteSpace(key)) { - throw new ArgumentNullException(nameof(key)); - } - - var value = GetValueByKey(key); - if (value is not null) { - return value; - } - - await UpsertAsync(key, @default); - return @default; - } - - /// - /// Gets the value associated with the specified key, or creates a new value if the key does not exist. - /// - /// The type of the value. - /// The key of the value. - /// The default value to create if the key does not exist. - /// The value associated with the key, or the created default value. - public async ValueTask GetOrCreateAsync(string key, T @default) where T : struct, IParsable { - var value = await GetOrCreateAsync(key, @default.ToString() ?? ""); - return T.Parse(value, CultureInfo.InvariantCulture); - } - - /// - /// Sets the specified key and value in the dictionary. - /// - /// The key to set. - /// The value to set. - protected virtual void SetKeyAndValue(string key, string value) => _dict![key] = value; - - /// - /// Inserts or updates a key-value pair in the dictionary and serializes. - /// - /// The key to insert or update. - /// The value to insert or update. - public virtual async ValueTask UpsertAsync(string key, string value) { - if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value)) { - return; - } - - // Skip updating if the key exists and the value is the same - var existingValue = GetValueByKey(key); - if (existingValue is not null && Equals(existingValue, value, StringComparison.Ordinal)) { - return; - } - - // Each call adds the key-value pair to the queue, and then tries to acquire the semaphore. - _queue.Enqueue(new(key, value)); - - // Concurrent calls, will be stuck here until the semaphore is released. - // Upon which the other thread inside might have already added the key-value pair to the dictionary. - // And serialized. - // We check after the release if anything left is in the queue and repeat the process. - // In perfect conditions with truly concurrent writes, it will cause the serialization to happen only once. - // Improving performance and reducing resource usage for writes. - try { - await _semaphore.WaitAsync(); - - if (_queue.IsEmpty) { - return; - } - - while (_queue.TryDequeue(out var item)) { - SetKeyAndValue(item.Key, item.Value); - } - await SerializeDictionaryAsync(); - } finally { - _semaphore.Release(); - } - - static bool Equals(ReadOnlySpan left, ReadOnlySpan right, StringComparison comparison) { - return left.Equals(right, comparison); - } - } - - /// - /// Upserts a value in the persistent dictionary based on the specified key. - /// - /// The type of the value. - /// The key to upsert the value for. - /// The value to upsert. - /// A representing the asynchronous operation. - public ValueTask UpsertAsync(string key, T value) where T : struct, IConvertible { - if (string.IsNullOrWhiteSpace(key)) { - throw new ArgumentNullException(nameof(key)); - } - - return UpsertAsync(key, value.ToString() ?? ""); - } - - /// - /// Deserializes the dictionary from its persisted state. - /// - protected abstract Dictionary? Deserialize(); - - /// - /// Removes all keys and values from the dictionary. - /// - /// A representing the asynchronous operation. - public virtual async ValueTask ClearAsync() { - if (Count is 0) { - return; - } - _dict.Clear(); - await SerializeDictionaryAsync(); - } - - /// - /// Serializes the contents of the dictionary to a persistent store. - /// - protected abstract Task SerializeAsync(); - - /// - /// Serializes the dictionary to a persistent store, while ensuring thread safety. - /// - /// It is executed automatically after . - public virtual async Task SerializeDictionaryAsync() => await SerializeAsync(); - - /// - /// Disposes of the resources used by the persistent dictionary. - /// - public void Dispose() { - if (_disposed) { - return; - } - _semaphore?.Dispose(); - _disposed = true; - } -} \ No newline at end of file diff --git a/src/Sharpify/Collections/RentedBufferWriter{T}.cs b/src/Sharpify/Collections/RentedBufferWriter{T}.cs deleted file mode 100644 index 818cccf..0000000 --- a/src/Sharpify/Collections/RentedBufferWriter{T}.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System.Buffers; -using System.Runtime.CompilerServices; - -namespace Sharpify.Collections; - -/// -/// A buffer writer that uses an array rented from the shared array pool -/// -/// -/// -/// Essentially an allocation free alternative to -/// -public sealed class RentedBufferWriter : IBufferWriter, IDisposable { - private readonly T[] _buffer; - private volatile bool _disposed; - - /// - /// The current position in the buffer - /// - public int Position { get; private set; } - - /// - /// The actual capacity of the rented buffer - /// - public readonly int ActualCapacity; - - /// - /// If the is disabled, it means that it is not usable, doesn't contain a backing array and all operations will throw an exception - /// - public readonly bool IsDisabled; - - /// - /// Creates a new rented buffer writer with the at least the given capacity - /// - /// - public static RentedBufferWriter Create(int capacity) => new(capacity); - - /// - /// Creates a new rented buffer writer with the at least the given capacity - /// - /// The actual buffer will be at least this size - public RentedBufferWriter(int capacity) { - ArgumentOutOfRangeException.ThrowIfNegative(capacity); - - if (capacity is 0) { - _buffer = Array.Empty(); - IsDisabled = true; - return; - } - _buffer = ArrayPool.Shared.Rent(capacity); - ActualCapacity = _buffer.Length; - } - - /// - public void Advance(int count) { - if (IsDisabled) { - throw new InvalidOperationException("The buffer writer is disabled."); - } - - ArgumentOutOfRangeException.ThrowIfNegative(count); - ArgumentOutOfRangeException.ThrowIfGreaterThan(Position, _buffer.Length - count); - - Position += count; - } - - /// - /// Attempts to write a sequence of elements to the buffer and advances the position - /// - /// - /// true if the operation is successful, false if there is not enough space available - /// if the buffer is disabled - public bool WriteAndAdvance(T item) { - if (IsDisabled) { - throw new InvalidOperationException("The buffer writer is disabled."); - } - - if (FreeCapacity is 0) { - return false; - } - - GetSpan()[0] = item; - Advance(1); - return true; - } - - /// - /// Attempts to write a sequence of elements to the buffer and advances the position - /// - /// - /// true if the operation is successful, false if there is not enough space available - /// if the buffer is disabled - public bool WriteAndAdvance(ReadOnlySpan data) { - if (IsDisabled) { - throw new InvalidOperationException("The buffer writer is disabled."); - } - - if (data.Length > FreeCapacity) { - return false; - } - - data.CopyTo(GetSpan()); - Advance(data.Length); - return true; - } - - /// - /// Returns the underlying buffer - /// - public T[] Buffer => _buffer; - - /// - /// Returns a readonly reference to the underlying buffer - /// - public ref T[] GetReferenceUnsafe() => ref Unsafe.AsRef(in _buffer); - - /// - /// Gets the portion of the free buffer that can be written to, beginning at - /// - /// Not regarded - /// - public Memory GetMemory(int sizeHint = 0) => _buffer.AsMemory(Position); - - /// - /// Gets the portion of the free buffer that can be written to, beginning at - /// - /// Not regarded - /// - public Span GetSpan(int sizeHint = 0) => _buffer.AsSpan(Position); - - /// - /// Gets the portion of the buffer that has been written to, beginning at index 0 - /// - public ArraySegment WrittenSegment => new(_buffer, 0, Position); - - /// - /// Gets the portion of the buffer that has been written to, beginning at index 0 - /// - public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, Position); - - /// - /// Gets the portion of the buffer that has been written to, beginning at index 0 - /// - public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, Position); - - /// - /// Returns the number of elements that can be written to the buffer - /// - public int FreeCapacity => _buffer.Length - Position; - - /// - /// Resets the buffer writer to its initial state by setting to 0 - /// - public void Reset() => Position = 0; - - /// - /// Returns a slice of the buffer - /// - /// - /// - /// - public ReadOnlyMemory GetMemorySlice(int start, int length) { - ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, _buffer.Length); - - return _buffer.AsMemory(start, length); - } - - /// - /// Returns a slice of the buffer - /// - /// - /// - /// - public ReadOnlySpan GetSpanSlice(int start, int length) { - ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, _buffer.Length); - - return _buffer.AsSpan(start, length); - } - - /// - /// Returns the rented buffer to the shared array pool - /// - public void Dispose() { - if (_disposed) { - return; - } - if (!IsDisabled) { - ArrayPool.Shared.Return(_buffer); - } - _disposed = true; - } -} \ No newline at end of file diff --git a/src/Sharpify/Collections/SortedList.cs b/src/Sharpify/Collections/SortedList.cs index 3686225..49df1ee 100644 --- a/src/Sharpify/Collections/SortedList.cs +++ b/src/Sharpify/Collections/SortedList.cs @@ -7,23 +7,27 @@ namespace Sharpify.Collections; /// /// The type of elements in the list. public class SortedList : IReadOnlyList { - /// - /// The underlying list used for storing elements in the SortedList. - /// - protected readonly List _list; - /// - /// The comparer used to compare elements in the sorted list. - /// - protected readonly IComparer _comparer; - /// - /// Gets a value indicating whether the SortedList allows duplicate elements. - /// - protected readonly bool _allowDuplicates; + /// + /// The underlying list used for storing elements in the SortedList. + /// +#pragma warning disable CA1051 // Do not declare visible instance fields + protected readonly List _list; + /// + /// The comparer used to compare elements in the sorted list. + /// - /// - /// Initializes a new instance of the class that is empty, has the default initial capacity, and uses the default comparer for the element type. - /// - public SortedList() : this(null, Comparer.Default, false) { } + protected readonly IComparer _comparer; + /// + /// Gets a value indicating whether the SortedList allows duplicate elements. + /// + + protected readonly bool _allowDuplicates; +#pragma warning restore CA1051 // Do not declare visible instance fields + + /// + /// Initializes a new instance of the class that is empty, has the default initial capacity, and uses the default comparer for the element type. + /// + public SortedList() : this(null, Comparer.Default, false) { } /// /// Initializes a new instance of the class that contains elements copied from the specified collection diff --git a/src/Sharpify/Collections/StringBuffer.cs b/src/Sharpify/Collections/StringBuffer.cs index 9d260f6..3b88483 100644 --- a/src/Sharpify/Collections/StringBuffer.cs +++ b/src/Sharpify/Collections/StringBuffer.cs @@ -10,7 +10,11 @@ public unsafe ref struct StringBuffer { /// /// The total length of the buffer. /// +#pragma warning disable CA1051 // Do not declare visible instance fields + public readonly int Length; +#pragma warning restore CA1051 // Do not declare visible instance fields + /// /// The current position of the buffer. @@ -76,7 +80,7 @@ public ref StringBuffer Append(ReadOnlySpan str) { public ref StringBuffer Append(T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable { bool appended = value.TryFormat(_buffer.Slice(Position), out var charsWritten, format, provider); if (!appended) { - throw new ArgumentOutOfRangeException(nameof(Length)); + throw new ArgumentOutOfRangeException(nameof(value), "Value did not fit into the buffer."); } Position += charsWritten; return ref this; diff --git a/src/Sharpify/Either.cs b/src/Sharpify/Either.cs index 14816c2..c06d5ec 100644 --- a/src/Sharpify/Either.cs +++ b/src/Sharpify/Either.cs @@ -10,12 +10,12 @@ public readonly record struct Either { /// /// Checks if the value is T0. /// - public readonly bool IsT0; + public bool IsT0 { get; } /// /// Checks if the value is T1. /// - public readonly bool IsT1; + public bool IsT1 { get; } /// /// Gets the value as T0. diff --git a/src/Sharpify/InternalHelper.cs b/src/Sharpify/InternalHelper.cs deleted file mode 100644 index 873cac4..0000000 --- a/src/Sharpify/InternalHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Sharpify; - -internal static class InternalHelper { - internal static readonly JsonReaderOptions JsonReaderOptions = new() { - AllowTrailingCommas = true, - CommentHandling = JsonCommentHandling.Skip - }; -} - -[JsonSourceGenerationOptions(WriteIndented = true)] -[JsonSerializable(typeof(Dictionary))] -internal partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/Sharpify/MonitoredSerializableObject{T}.cs b/src/Sharpify/MonitoredSerializableObject{T}.cs deleted file mode 100644 index ac08f39..0000000 --- a/src/Sharpify/MonitoredSerializableObject{T}.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Sharpify; - -/// -/// Represents a that is monitored for changes from the file system. -/// -/// The type of the value stored in the object. -/// -/// This class provides functionality to serialize and deserialize the object to/from a file, -/// and raises an event whenever the file or the object is modified. -/// -public class MonitoredSerializableObject : SerializableObject { - private readonly FileSystemWatcher _watcher; - - /// - /// Represents a serializable object that is monitored for changes in a specified file path. - /// - /// The path to the file. validated on creation - /// The json type info that can be used to serialize T without reflection - /// Thrown when the directory of the path does not exist or when the filename is invalid. - public MonitoredSerializableObject(string path, JsonTypeInfo jsonTypeInfo) : this(path, default!, jsonTypeInfo) { } - - /// - /// Represents a serializable object that is monitored for changes in a specified file path. - /// - /// The path to the file. validated on creation - /// the default value of T, will be used if the file doesn't exist or can't be deserialized - /// The json type info that can be used to serialize T without reflection - /// Thrown when the directory of the path does not exist or when the filename is invalid. - public MonitoredSerializableObject(string path, T defaultValue, JsonTypeInfo jsonTypeInfo) : base(path, defaultValue, jsonTypeInfo) { - _watcher = new FileSystemWatcher(_segmentedPath.Directory, _segmentedPath.FileName) { - NotifyFilter = NotifyFilters.LastWrite, - EnableRaisingEvents = true - }; - - _watcher.Changed += OnFileChanged; - } - - private void OnFileChanged(object sender, FileSystemEventArgs e) { - if (e.ChangeType is not WatcherChangeTypes.Changed) { - return; - } - if (!File.Exists(_path)) { - return; - } - try { - _lock.EnterWriteLock(); - var json = File.ReadAllText(_path); - _value = JsonSerializer.Deserialize(json, _jsonTypeInfo)!; - InvokeOnChangedEvent(_value); - } catch { - // ignore - } finally { - _lock.ExitWriteLock(); - } - } - - /// - public override void Modify(Func modifier) { - _watcher.EnableRaisingEvents = false; - base.Modify(modifier); - _watcher.EnableRaisingEvents = true; - } - - /// - public override void Dispose() { - if (_disposed) { - return; - } - _watcher?.Dispose(); - _lock?.Dispose(); - _disposed = true; - } -} \ No newline at end of file diff --git a/src/Sharpify/ParallelExtensions.cs b/src/Sharpify/ParallelExtensions.cs index 2f21aa5..71225f9 100644 --- a/src/Sharpify/ParallelExtensions.cs +++ b/src/Sharpify/ParallelExtensions.cs @@ -1,3 +1,5 @@ +using System.Buffers; + using Sharpify.Collections; namespace Sharpify; @@ -25,17 +27,14 @@ public static async Task ForAll( var length = collection.Count; - using var taskBuffer = new RentedBufferWriter(length); + using var taskArrayOwner = ArrayPool.Shared.Rent(length, out Task[] array); + var taskBuffer = BufferWrapper.Create(array); foreach (var item in collection) { - taskBuffer.WriteAndAdvance(body.Invoke(item, token)); + taskBuffer.Append(body.Invoke(item, token)); } -#if NET9_0_OR_GREATER await Task.WhenAll(taskBuffer.WrittenSpan).WaitAsync(token).ConfigureAwait(false); -#else - await Task.WhenAll(taskBuffer.WrittenSegment).WaitAsync(token).ConfigureAwait(false); -#endif } /// @@ -77,17 +76,14 @@ public static async Task ForAllAsync( var length = collection.Count; - using var taskBuffer = new RentedBufferWriter(length); + using var taskArrayOwner = ArrayPool.Shared.Rent(length, out Task[] array); + var taskBuffer = BufferWrapper.Create(array); foreach (var item in collection) { - taskBuffer.WriteAndAdvance(Task.Run(() => body.Invoke(item, token), token)); + taskBuffer.Append(Task.Run(() => body.Invoke(item, token), token)); } -#if NET9_0_OR_GREATER await Task.WhenAll(taskBuffer.WrittenSpan).WaitAsync(token).ConfigureAwait(false); -#else - await Task.WhenAll(taskBuffer.WrittenSegment).WaitAsync(token).ConfigureAwait(false); -#endif } /// diff --git a/src/Sharpify/PooledArrayOwner.cs b/src/Sharpify/PooledArrayOwner.cs new file mode 100644 index 0000000..753333f --- /dev/null +++ b/src/Sharpify/PooledArrayOwner.cs @@ -0,0 +1,75 @@ +using System.Buffers; + +namespace Sharpify; + +/// +/// A struct that allows renting an array from and manage its return using the interface. +/// +/// +public partial struct PooledArrayOwner : IDisposable { + private readonly ArrayPool _pool; + private bool _disposed; + + /// + /// The rented array held by this object. + /// + public T[] Value { get; private set; } + + internal PooledArrayOwner(ArrayPool pool, int minimumLength) { + _pool = pool; + Value = _pool.Rent(minimumLength); + } + + /// + /// Returns the rented array back to . + /// + public void Dispose() { + if (_disposed) return; + _pool.Return(Value); + _disposed = true; + } +} + +/// +/// Provides any extensions that return a . +/// +public static class ArrayPoolExtensions { + /// + /// Rent an array with from an , returning a struct that will return it after being disposed, and also the held array reference. + /// + /// + /// + /// + /// + public static PooledArrayOwner Rent(this ArrayPool pool, int minimumLength, out T[] array) { + PooledArrayOwner owner = new(pool, minimumLength); + array = owner.Value; + return owner; + } + + /// + /// Rent an array with from an , returning a struct that will return it after being disposed, and also a over the held array reference. + /// + /// + /// + /// + /// + public static PooledArrayOwner Rent(this ArrayPool pool, int minimumLength, out Span span) { + PooledArrayOwner owner = new(pool, minimumLength); + span = owner.Value.AsSpan(); + return owner; + } + + /// + /// Rent an array with from an , returning a struct that will return it after being disposed, and also a over the held array reference. + /// + /// + /// + /// + /// + public static PooledArrayOwner Rent(this ArrayPool pool, int minimumLength, out Memory memory) { + PooledArrayOwner owner = new(pool, minimumLength); + memory = owner.Value.AsMemory(); + return owner; + } +} \ No newline at end of file diff --git a/src/Sharpify/README.md b/src/Sharpify/README.md deleted file mode 100644 index 3482a20..0000000 --- a/src/Sharpify/README.md +++ /dev/null @@ -1,468 +0,0 @@ -# Sharpify - -A collection of high performance language extensions for C# - -## Why Sharpify? - -Sharpify is a collection of commonly used language extensions, that usually people re-write in each project, This is an alternative where you can use the same convenient functionality, and gain the performance and reliability of using a highly optimized and extensively tested library. Sharpify has minimal footprint and is fully AOT compatible, so it can be used virtually in any project. - -## Features - -* ⚡ Fully Native AOT compatible -* 🤷 `Either` - Discriminated union object that forces handling of both cases -* 🦾 Flexible `Result` type that can encapsulate any other type and adds a massage options and a success or failure status. Flexible as it doesn't require any special handling to use (unlike `Either`) -* 🏄 Wrapper extensions that simplify use of common functions and advanced features from the `CollectionsMarshal` class -* `Routine` and `AsyncRoutine` bring the user easily usable and configurable interval based background job execution. -* `PersistentDictionary` and derived types are super lightweight and efficient serializable dictionaries that are thread-safe and work amazingly for things like configuration files. -* `SortedList` bridges the performance of `List` and order assurance of `SortedSet` -* `PersistentDictionary` and variants provide all simple database needs, with perfected performance and optimized concurrency. -* `SerializableObject` and the `Monitored` variant allow persisting an object to the disk, and elegantly synchronizing modifications. -* 💿 `StringBuffer` enables zero allocation, easy to use appending buffer for creation of strings in hot paths. -* `RentedBufferWriter{T}` is an allocation friendly alternative to `ArrayBufferWriter{T}` for hot paths. -* A 🚣🏻 boat load of extension functions for all common types, bridging ease of use and performance. -* `Utils.DateAndTime`, `Utils.Env`, `Utils.Math`, `Utils.Strings` and `Utils.Unsafe` provide uncanny convenience at maximal performance. -* 🧵 `ThreadSafe` makes any variable type thread-safe -* 🔐 `AesProvider` provides access to industry leading AES-128 encryption with virtually no setup -* 🏋️ High performance optimized alternatives to core language extensions -* 🎁 More added features that are not present in the core language -* ❗ Static inner exception throwers guide the JIT to further optimize the code during runtime. -* 🫴 Focus on giving the user complete control by using flexible and common types, and resulting types that can be further used and just viewed. - -## Installation - -[![Nuget](https://img.shields.io/nuget/dt/Sharpify?label=Sharpify%20Nuget%20Downloads)](https://www.nuget.org/packages/Sharpify/) -> dotnet add package Sharpify - -## Usage - -### Collection Extensions - -```csharp -bool IsNullOrEmpty(this ICollection? collection); -Span AsSpan(this List list); -ref TValue GetValueRefOrNullRef(this Dictionary dictionary, TKey key) where TKey : notnull {}; -ref TValue? GetValueRefOrAddDefault(this Dictionary dictionary, TKey key, out bool exists) where TKey : notnull {}; -void CopyTo(this Dictionary dict, KeyValuePair[] array, int index) : where TKey : notnull {}; -(KeyValuePair[] rentedBuffer, ArraySegment> entries) RentBufferAndCopyEntries(this Dictionary dict) where TKey : notnull {}; -void ReturnBufferToSharedArrayPool(this T[] arr); -void ReturnBufferToArrayPool(this T[] arr, ArrayPool pool); -T[] PureSort(this T[] source, IComparer comparer); -List PureSort(this IEnumerable source, IComparer comparer); -void RemoveDuplicatesFromSorted(this List list, IComparer comparer); -void RemoveDuplicates(this List list, IEqualityComparer? comparer = null, bool isSorted = false); -void RemoveDuplicates(this List list, out HashSet hSet, IEqualityComparer? comparer = null, bool isSorted = false); -List> ChunkToSegments(this T[] arr, int sizeOfChunk); -int CopyToArray(this HashSet hashSet, T[] destination, int index); -``` - -### Unmanaged Extensions - -```csharp -bool TryParseAsEnum(this string value, out TEnum result) where TEnum : struct, Enum; -``` - -### String Extensions - -```csharp -ref char GetReference(this string text); // Returns actual reference to first character -bool IsNullOrEmpty(this string str); -bool IsNullOrWhiteSpace(this string str); -// Tried to convert the string to int32 using ascii, very efficient -bool TryConvertToInt32(this ReadOnlySpan value, out int result); -// Concat - more efficient than + or interpolation -string Concat(this string value, ReadOnlySpan suffix); -string ToTitle(this string str); -bool IsBinary(this string str); -``` - -### Utils - -Utils is a static class that provides advanced options that wouldn't be straight forward as extensions, it has static subclasses that are classified by their area of operation. - -#### Utils.DateAndTime - -```csharp -// Returns the date time as value task that can be fired and awaited to be later used, removing the need to synchronously wait for it -ValueTask GetCurrentTimeAsync(); -// Same but returns the binary representation -ValueTask GetCurrentTimeInBinaryAsync(); -// Formats time span into a span of characters and returns the written portion -ReadOnlySpan FormatTimeSpan(TimeSpan timeSpan, Span buffer); -// Formats the time span into a string -string FormatTimeSpan(TimeSpan timeSpan); -// Formats the time stamp into a span of characters and returns the written portion -ReadOnlySpan FormatTimeStamp(DateTime time, Span buffer); -// Formats the time stamp into a string -string FormatTimeStamp(DateTime time); -``` - -#### Utils.Env - -```csharp -bool IsRunningOnWindows(); -bool IsRunningAsAdmin(); -string GetBaseDirectory(); -bool IsInternetAvailable; -string PathInBaseDirectory(string filename); // Returns a filename combined with the base directory -void OpenLink(string url); // semi-cross-platform (works on Windows, Mac and Linux) -``` - -#### Utils.Mathematics - -```csharp -double RollingAverage(double oldAverage, double newNumber, int sampleCount); -double Factorial(double n); -double FibonacciApproximation(int n); -``` - -#### Utils.Strings - -```csharp -// Format bytes into to a text containing the largest storage unit with 2 decimals places and the storage unit -// i.e 5.23 MB or 6.77 TB and so on... -string FormatBytes(double bytes); -string FormatBytes(long bytes); -// Formats the bytes in the same format to a buffer and returns the written span -ReadOnlySpan FormatBytes(double bytes, Span buffer); -ReadOnlySpan FormatBytes(long bytes, Span buffer); -``` - -#### Utils.Unsafe - -```csharp -// Creates converts a predicate to a function that returns an integer value 0 or 1 -Func CreateIntegerPredicate(Func predicate); -// Converts a readonly span to a mutable span -unsafe Span AsMutableSpan(ReadOnlySpan span); -// Tries to unbox a valuetype from the heap -bool TryUnbox(object obj, out T value) where T : struct; -``` - -### Union Types - -This library contains 2 discriminate union types, to suit a wide variety of user needs. - -#### 1. `Result` / `Result` - -This type works by having a `bool IsOk` property, a `string Message` and if needed a `T? Value` property. This allows the type to remain the same regardless of error or success, thus it is performs way better than lambda-required-handling types, but is more suited when the consumer knows exactly how it works. - -For example, when the `Result` is a failure, `T? Value` will be `null` this means, that if someone skips the check or tries to access the value when the `Result` is a failure, they will get an exception. - -* Both types are readonly structs but will throw an `InvalidOperation` exception if they are created using a default constructor. The only valid way to create them is using the static factory methods inside `Result`. -* Both `Result` and `Result` also have the methods `.AsTask()` and `.AsValueTask()` that wrap a `Task` or `ValueTask` around them to make them easier to use in non-async `Task` or `ValueTask` methods. -* `Result` has an extension method called `.WithValue(T Value`, which will return a `Result` with the same `Message` and `IsOk` values. However, it is not recommended to use as the performance is worse than the factory methods, and it allows adding a non-`null` `Value` to a failed `Result` which messes with the logic flow. - -#### 2. `Either` - -This type is your usual lambda-required-handling discriminated union type, similar to `OneOf`. However it only has an option for 2 types. - -This type has implicit operators that cast any of `T0` or `T1` to the type, and requires the consumer to either use delegates to get access to each, or to force casting it one type or the other. As with `OneOf` this makes it a little bit safer to use but vastly impacts performance, especially if you need to take the output value of one of them and continue processing it outside the lambda, or if you want to propagate a certain result forward in the code flow. - -### ThreadSafe - -`ThreadSafe` is a special wrapper instance type that can make any other type thread-safe to be used in concurrency. - -It works by having a lock and limiting modification access to a single thread at a time. - -You can access the value any time by using `ThreadSafe.Value`. - -and modify the value using the following method: - -```csharp -public T Modify(Func modificationFunc) -``` - -This both modifies the value and returns the result after modification. - -### AesProvider - -`AesProvider` is a class that implements `IDisposable` and allows very easy usage of AES128. - -#### Static Methods - -```csharp -string GeneratePassword(string password, int iterations = 991); -``` - -This generates a hashed password from a real password. This is useful for storing and verifying account credentials. - -```csharp -bool IsPasswordValid(string password, string hashedPassword); -``` - -This will verify a hashed password against a real password. - -#### Instance Methods - -Constructor: - -```csharp -public AesProvider(string strKey); -``` - -Unlike the base classes of the language, this takes a key in the format of a `string`, and does all the magic of handling length, padding and etc, by itself, so that you only need the key to encrypt or decrypt. - -Remember that this class implements `IDisposable`, make sure to dispose of it properly or use the `using` keyword or block. - -These methods handle regular encryption: - -```csharp -// Encryption -string Encrypt(ReadOnlySpan unencrypted); -string EncryptUrl(string url); -byte[] EncryptBytes(ReadOnlySpan unencrypted); -int EncryptBytes(ReadOnlySpan unencrypted, Span destination); -// Decryption -string Decrypt(string encrypted); -string DecryptUrl(string encryptedUrl); -byte[] DecryptBytes(ReadOnlySpan encrypted, bool throwOnError = false); -int DecryptBytes(ReadOnlySpan encrypted, Span destination, bool throwOnError = false); -``` - -These methods handle base64 encryption (for usage with URLs, filenames and etc.): - -```csharp -string EncryptUrl(string url); -string DecryptUrl(string encryptedUrl); -``` - -For more advanced encryption or decryption you can use these: - -```csharp -ICryptoTransform CreateEncryptor(); -ICryptoTransform CreateDecryptor(); -``` - -These will create an `ICryptoTransform` using the key and configuration from the `AesProvider` instance. You can use it to encrypt and decrypt using streams and many more advanced options. - -`ICryptoTransform` is also implementing `IDisposable` make sure to handle it appropriately. - -### Collections - -Sharpify has multiple custom collections such as: - -#### SortedList{T} - -`SortedList` is a re-implementation of `List` with custom crud operations: - -* Add -> O(log n) -* Remove -> O(log n) -* Get by sorted index O(1) - i.e min is [0] and max is [length - 1], also second max is [length - 2]... -* Option to disallow duplicates - -The `SortedList` also has convenience features, such as `AsSpan`, `Clear` methods, exposure of the `List.Enumerator` which is an efficient struct, and also an implicit operator which can return the inner list in places which require `List` (however be careful as the receiver may use the inner list and it may no longer maintain the features above) - -#### PersistentDictionary - -`PersistentDictionary` is a thread-safe `Dictionary(string key, T value) where T : struct, IConvertible -public virtual async ValueTask UpsertAsync(string key, string value) -// Retrieval -public async ValueTask GetOrCreateAsync(string key, T @default) where T : struct, IParsable -public virtual async ValueTask GetOrCreateAsync(string key, string @default) -``` - -you can also get values with a synchronous operation if you require by using `PersistentDictionary[key]` - -but upsert is not available asynchronously due to the synchronization mechanisms that are used to optimize the concurrency - -To configure the type for usage you can implement the class and it will show you the specific things required to make everything work. In addition, there are 2 built-in implementations: - -* `LocalPersistentDictionary` is an implementation that serializes and restores the dictionary from a local path -* `LazyLocalPersistentDictionary` is an implementation that also serializes and restores the dictionary from a local path, doesn't maintain an in-memory version, allowing it to be garbage collected if it was even created, this is for very memory constrained scenarios. Reading from it, doesn't even create a dictionary. - -#### StringBuffer - -`StringBuffer` is a ref struct that encapsulates a `Span{char}` and allow very efficient appending of characters, strings and other `ISpanFormattable` implementations. It enables usage patterns similar to that of `StringBuilder` but with a much lower memory footprint, and can work on stack allocated buffers. - -It uses internal indexes to properly append elements, requiring no tracking from the user. - -`StringBuffer` is created with a factory method named `Create(Span)` that creates and returns an instance of `StringBuffer` with the specified buffer. - -As it is a `ref struct`, it does have a default constructor, using it will create an instance on an empty buffer, that will throw an exception if you try to append anything to it. Please refrain from using it, it is only public because of compiler limitations. - -##### Appending - -```csharp -ref TBuffer Append(char c); -ref TBuffer Append(ReadOnlySpan str); -ref TBuffer Append(T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable {} -ref TBuffer AppendLine(); -ref TBuffer AppendLine(char c); -ref TBuffer AppendLine(ReadOnlySpan str); -ref TBuffer AppendLine(T value, ReadOnlySpan format = default, IFormatProvider? provider = null) where T : ISpanFormattable {}; -``` - -All the append methods return a reference to self, this is to enable usage of the builder pattern. - -##### Finalization - -```csharp -// Will return the written portion of the buffer -WrittenSpan; -// Will create a string from the written portion of the buffer while removing end white spaces if they exist -Allocate(bool trimIfShorter, bool trimEndWhiteSpace); -Allocate(bool trimIfShorter); // ~ trimEndWhiteSpace = false -Allocate(); // ~ trimIfShorter = true, trimEndWhiteSpace = false -ToString() // Will call Allocate(true, false) -``` - -##### Example - -```csharp -public string GetHello() { - var buffer = StringBuffer.Create(stackalloc char[50]); - buffer.Append("Hello"); - buffer.Append(' '); - buffer.Append("Everyone"); - buffer.Append('!'); - return buffer.Allocate(); - // We sample text is separated for api showcase. -} -// The returned result will be "Hello Everyone!" -``` - -```csharp -// Example of usage with the builder pattern - similar to StringBuilder -public string GetHello() { - return StringBuffer.Create(stackalloc char[50]) - .Append("Hello") - .Append(' ') - .Append("Everyone") - .Append('!') - .Allocate(); -} -``` - -#### RentedBufferWriter{T} - -`RentedBufferWriter{T}` is an allocation friendly alternative to `ArrayBufferWriter{T}` which implements `IBufferWriter{T}`, an interface that represent a bucket that data can be written to. while it is not a commonly used interface, created to optimize specific hot paths, such as networking and IO pipes, using them is not very straightforward, and while `ArrayBufferWriter{T}` is a rather useful tool for some cases, it's limitation is that it isn't bound to any capacity, thus it always allocates arrays, and when it runs out of space, it allocates bigger arrays to resize, and that puts unneeded pressure of the GC. - -`RentedBufferWriter{T}` fixes this by restricting the capacity at initialization, and renting the buffer from the shared array pool. Note that `SizeHint` in `GetSpan` and `GetMemory` is completely ignored in this implementation as resizing the inner buffer is currently not possible, by design. In case you are not sure what can exact capacity needed is, overestimate, it won't have much negative effects on the shared array pool. - -Aside from implementing the interface `IBufferWriter{T}`, it also explicitly implements `IDisposable` to make sure the inner buffer is returned to the shared array pool after use. And implements many convenience methods and properties, such as: - -```csharp -int ActualCapacity; -int FreeCapacity; -int Position { get; private set; } -ReadOnlySpan WrittenSpan; -ReadOnlyMemory WrittenMemory; -ArraySegment WrittenSegment; -ReadOnlySpan GetSpanSlice(int start, int length); -ReadOnlyMemory GetMemorySlice(int start, int length); -void Advance(int count); -bool WriteAndAdvance(T item); -bool WriteAndAdvance(ReadOnlySpan data); -void Reset(); -T[] Buffer; // Which returns the instance of the inner buffer, be careful with this. -ref T[] GetReferenceUnsafe(); // returns the reference for the inner buffer, be extra careful with this -``` - -### Routines - -Routines are special objects that contain a collection of actions and executes them once a specified interval has passed, until it is stopped. This was inspired by Coroutines from the Unity game engine. - -#### Routine - -The default `Routine` is the simplest version, it has a collection of `Action`s and once every timer interval has passed they get executed sequentially. - -#### AsyncRoutine - -`AsyncRoutine` is more complicated but more precise and flexible, by providing a `CancellationTokenSource`, the `AsyncRoutine` can create tasks that will all run only while the source isn't cancelled. - -In addition it can be initialized with a `RoutineOptions` that configures 2 important parameters: - -1. `ThrowOnCancellation` will cause an exception to be thrown when the source is cancelled, if left out, the routine will gracefully cancel all the tasks and exit quietly. -2. `ExecuteInParallel` will configure the routine to execute the tasks concurrently and not sequentially which is the default. - -### Special Types - -#### SerializableObject{T} - -`SerializableObject{T}` is a wrapper around a reference or value type, that will serialize the inner value to a file, monitor the file to synchronize external changes, and notify of changes via an `OnChanged` event. - -The simplest use-case of this is for example you create a `record` for your app settings, which then enables each setting to be type safe and specific. Then when you change it from code. - -A `JsonTypeInfo` for the type is required, it will makes it more performant and AOT compatible. - -##### Initialization - -```csharp -new SerializableObject(string path, T defaultValue, JsonTypeInfo jsonTypeInfo); -new SerializableObject(string path, JsonTypeInfo jsonTypeInfo); // uses the other constructor with the default{T} -``` - -The constructor first validates the path, if the directory doesn't exist or filename is empty, it will throw a `IOException`, if the file doesn't exist, or the contents of the file are empty, it will serialize the default value to the file, otherwise it will deserialize from the file or set to the default if it fails. - -In case you never created a `JsonSerializerContext`, this is how: -imagine for the example that the object type is `Configuration` - -```csharp -// This needs to be under the namespace, it cannot be a nested class. -[JsonSourceGenerationOptions(WriteIndented = true)] // Optional -[JsonSerializable(typeof(Configuration))] -internal partial class JsonContext : JsonSerializerContext { } -// The source generator will take care of everything. - -// Now an example of creating the object -public static readonly SerializableObject Config = new(_path, JsonContext.Default.Configuration); -// Notice how we passed the JsonContext -``` - -##### Modification - -```csharp -void Modify(Func modifier) -``` - -Modification is done using a function, this is to both enable an experience similar to `options` and to make it work with `struct`s because they are value types. - -```csharp -Modify(person => { - person.Name = "New"; - return person; -}); // Simple change that will work with reference types or value types -// If person was a record, it is even easier -Modify(person => person with { Name = "New" }); -``` - -##### Subscribing And Notifications - -The event that notifies for changes is `OnChanged`, and you need to subscribe to it with a signature of `void Function(object sender, SerializedObjectEventArgs e)`, this is a special event args implementation that will contain the new value after the change. an anonymous function with the same parameters is also accepted. - -For example: - -```csharp -var serializedObj = new SerializableObject(path, new Person { Name = "Dave" }, JsonSerializerContext.Default.Person); -monitoredObj.OnChanged += OnValueChanged - -private void OnValueChanged(object sender, SerializedObjectEventArgs e) { - Console.WriteLine($"The new name is {e.Value.Name}"); -} -``` - -This basically concludes the general usage. - -#### MonitoredSerializableObject{T} - -`MonitoredSerializableObject{T}` is an extension of `SerializableObject{T}` which adds functionality of watching the filesystem to synchronize external changes, usage is basically identical except `MonitoredSerializableObject{T}` also implements `IDisposable` to release the resources of the file system watcher. - -### Notes - -* In order to avoid file writing exceptions, `Modify` is synchronized using a lock, to only be performed by a single thread at a time. -* There is also an internal mechanism that should prevent deserialization after internal modification in order to reduce io operations and redundant value updates. -* Both variants of `SerializableObject{T}` implement `IDisposable` and should be disposed of properly, but their main use-case is to be initialized once and used throughout the lifetime of the application, so this isn't absolutely crucial, and they both implement a finalizer that will dispose of the resources anyway. - -## Contact - -For bug reports, feature requests or offers of support/sponsorship contact - -> This project is proudly made in Israel 🇮🇱 for the benefit of mankind. diff --git a/src/Sharpify/Routines/AsyncRoutine.cs b/src/Sharpify/Routines/AsyncRoutine.cs index 7629674..c07fa40 100644 --- a/src/Sharpify/Routines/AsyncRoutine.cs +++ b/src/Sharpify/Routines/AsyncRoutine.cs @@ -18,7 +18,7 @@ public class AsyncRoutine : IDisposable { /// /// List of asynchronous actions to be executed. /// - public readonly List> Actions = []; + public List> Actions { get; private set; } /// /// Initializes a new instance of the class with the specified interval, options, and cancellation token source. @@ -27,6 +27,7 @@ public class AsyncRoutine : IDisposable { /// The options to configure the behavior of the routine. /// The cancellation token source used to cancel the routine. public AsyncRoutine(TimeSpan interval, RoutineOptions options, CancellationTokenSource cancellationTokenSource) { + Actions = []; _options = options; _timer = new PeriodicTimer(interval); _isRunning = true; @@ -90,16 +91,13 @@ public async Task Start() { } // Execute in Parallel if (_options.HasFlag(RoutineOptions.ExecuteInParallel)) { - using var buffer = new RentedBufferWriter(Actions.Count); + using var taskArrayOwner = ArrayPool.Shared.Rent(Actions.Count, out Task[] array); + var buffer = BufferWrapper.Create(array); foreach (var action in Actions) { - buffer.WriteAndAdvance(Task.Run(() => action(_cancellationTokenSource.Token) + buffer.Append(Task.Run(() => action(_cancellationTokenSource.Token) , _cancellationTokenSource.Token)); } -#if NET9_0_OR_GREATER await Task.WhenAll(buffer.WrittenSpan).WaitAsync(_cancellationTokenSource.Token).ConfigureAwait(false); -#else - await Task.WhenAll(buffer.WrittenSegment).WaitAsync(_cancellationTokenSource.Token).ConfigureAwait(false); -#endif // Execute sequentially } else { foreach (var action in Actions) { diff --git a/src/Sharpify/Routines/Routine.cs b/src/Sharpify/Routines/Routine.cs index d67b1a6..2bd1c98 100644 --- a/src/Sharpify/Routines/Routine.cs +++ b/src/Sharpify/Routines/Routine.cs @@ -10,13 +10,14 @@ public class Routine : IDisposable { /// /// List of actions to be executed by the routine. /// - public readonly List Actions = []; + public List Actions { get; private set; } /// /// Initializes a new instance of the class with the specified interval. /// /// The time interval between timer events, in milliseconds. public Routine(double intervalInMilliseconds) { + Actions = []; _timer = new System.Timers.Timer(intervalInMilliseconds); _timer.Elapsed += OnTimerElapsed; } diff --git a/src/Sharpify/SerializableObjectEventArgs.cs b/src/Sharpify/SerializableObjectEventArgs.cs deleted file mode 100644 index 1d7dc33..0000000 --- a/src/Sharpify/SerializableObjectEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Sharpify; - -/// -/// Represents the event arguments for a serializable object. -/// -public class SerializableObjectEventArgs : EventArgs { - /// - /// Gets the value associated with the event. - /// - public T Value { get; } - - /// - /// Initializes a new instance of the class with the specified value. - /// - /// The value associated with the event. - public SerializableObjectEventArgs(T value) => Value = value; -} \ No newline at end of file diff --git a/src/Sharpify/SerializableObject{T}.cs b/src/Sharpify/SerializableObject{T}.cs deleted file mode 100644 index 8dcd7ff..0000000 --- a/src/Sharpify/SerializableObject{T}.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Sharpify; - -/// -/// Represents a generic serializable object -/// -/// The type of the value stored in the object. -/// -/// This class provides functionality to serialize and deserialize the object to/from a file, -/// and raises an event whenever the object is modified. -/// -public class SerializableObject : IDisposable { - /// - /// The value of the SerializableObject. - /// - protected T _value = default!; - - /// - /// Gets value of type T. - /// - public T Value { - get { - _lock.EnterReadLock(); - try { - return _value; - } finally { - _lock.ExitReadLock(); - } - } - } - - /// - /// A value indicating whether the object has been disposed. - /// - protected volatile bool _disposed; - - /// - /// The path of the serialized object. - /// - protected readonly string _path; - - /// - /// The segmented path of the serialized object. - /// - protected readonly SegmentedPath _segmentedPath; - - /// - /// The JSON type info used for serializing and deserializing objects. - /// - protected readonly JsonTypeInfo _jsonTypeInfo; - - /// - /// The lock object used for thread synchronization. - /// - protected readonly ReaderWriterLockSlim _lock = new(); - - /// - /// Represents a serializable object that is monitored for changes in a specified file path. - /// - /// The path to the file. validated on creation - /// The json type info that can be used to serialize T without reflection - /// Thrown when the directory of the path does not exist or when the filename is invalid. - public SerializableObject(string path, JsonTypeInfo jsonTypeInfo) : this(path, default!, jsonTypeInfo) { } - - /// - /// Represents a serializable object that is monitored for changes in a specified file path. - /// - /// The path to the file. validated on creation - /// the default value of T, will be used if the file doesn't exist or can't be deserialized - /// The json type info that can be used to serialize T without reflection - /// Thrown when the directory of the path does not exist or when the filename is invalid. - public SerializableObject(string path, T defaultValue, JsonTypeInfo jsonTypeInfo) { - _jsonTypeInfo = jsonTypeInfo; - var dir = Path.GetDirectoryName(path); - var fileName = Path.GetFileName(path); - if (string.IsNullOrWhiteSpace(dir)) { - throw new IOException("The directory of path does not exist"); - } - if (string.IsNullOrWhiteSpace(fileName)) { - throw new IOException("Filename is invalid"); - } - _segmentedPath = new SegmentedPath(dir, fileName); - _path = path; - if (File.Exists(path)) { - var fileContent = File.ReadAllText(path); - if (fileContent.Length is 0) { - SetValueAndSerialize(defaultValue); - } else { - try { - var val = JsonSerializer.Deserialize(fileContent, _jsonTypeInfo); - if (val is null) { - SetValueAndSerialize(defaultValue); - } - _value = val!; - } catch { - SetValueAndSerialize(defaultValue); - } - } - } else { - if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) { - throw new ArgumentException("The provided path is an existing directory."); - } - SetValueAndSerialize(defaultValue); - } - } - - private void SetValueAndSerialize(T value) { - try { - _lock.EnterWriteLock(); - _value = value; - var json = JsonSerializer.Serialize(_value, _jsonTypeInfo); - File.WriteAllText(_path, json); - } finally { - _lock.ExitWriteLock(); - } - } - - /// - /// Represents the method that will handle the OnChanged" event. - /// - /// The source of the event. - /// An that contains the event data. - public delegate void OnChangedEventHandler(object sender, SerializableObjectEventArgs e); - - /// - /// Event that is raised when the has changed. - /// - public event OnChangedEventHandler? OnChanged; - - /// - /// Invokes the OnChanged event with the specified value. - /// - /// The value to pass to the event handler. - protected void InvokeOnChangedEvent(T value) { - OnChanged?.Invoke(this, new SerializableObjectEventArgs(value)); - } - - /// - /// Modifies the value of the object and performs necessary operations such as serialization and event invocation. - /// - /// The action that modifies the value of the object. - /// - /// - /// a lock is used to prevent concurrent modifications - /// - /// - /// When a record (non-struct) is used, do not use the "with" keyword to return a modification, this will allocate a new object, instead modify the existing and return the object, this will circularly exchange the reference. - /// - /// - public virtual void Modify(Func modifier) { - SetValueAndSerialize(modifier(_value)); - InvokeOnChangedEvent(_value); - } - - /// - public virtual void Dispose() { - if (_disposed) { - return; - } - _lock.Dispose(); - _disposed = true; - } - - /// - /// Represents a segmented path consisting of a directory and a file name. - /// - protected readonly record struct SegmentedPath(string Directory, string FileName); -} \ No newline at end of file diff --git a/src/Sharpify/Sharpify.csproj b/src/Sharpify/Sharpify.csproj index 99be670..fb9c44e 100644 --- a/src/Sharpify/Sharpify.csproj +++ b/src/Sharpify/Sharpify.csproj @@ -1,14 +1,13 @@ - net9.0;net8.0 + net10.0 enable - 2.5.0 + preview + 3.0.1 enable true David Shnayder David Shnayder - MIT - CHANGELOGLATEST.md True Sharpify A collection of high performance language extensions for C# @@ -20,16 +19,39 @@ true false true + + true + true + true + latest-recommended + true + + David Shnayder + David Shnayder + True + + README.md + MIT - - - + + + portable + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + - + + + + + diff --git a/src/Sharpify/Sharpify.csproj.DotSettings b/src/Sharpify/Sharpify.csproj.DotSettings deleted file mode 100644 index 89316e4..0000000 --- a/src/Sharpify/Sharpify.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - Library \ No newline at end of file diff --git a/src/Sharpify/StringExtensions.cs b/src/Sharpify/StringExtensions.cs index 9e40f9a..a49024b 100644 --- a/src/Sharpify/StringExtensions.cs +++ b/src/Sharpify/StringExtensions.cs @@ -15,63 +15,6 @@ public static ref char GetReference(this string text) { return ref Unsafe.AsRef(in text.GetPinnableReference()); } - /// - /// A simple wrapper over to make it easier to use. - /// - public static bool IsNullOrEmpty(this string str) => string.IsNullOrEmpty(str); - - /// - /// A simple wrapper over to make it easier to use. - /// - public static bool IsNullOrWhiteSpace(this string str) => string.IsNullOrWhiteSpace(str); - - /// - /// Tries to convert to an . - /// - /// The span of characters to convert. - /// When this method returns, contains the converted if the conversion succeeded, or zero if the conversion failed. - /// true if the conversion succeeded; otherwise, false. - public static bool TryConvertToInt32(this ReadOnlySpan value, out int result) { - result = 0; - if (value.IsWhiteSpace() || value.Length > 11) { // 10 is the max length of an int32 + 1 for sign - return false; - } - bool isNegative = value[0] is '-'; - var length = value.Length; - int i = 0; - if (isNegative) { - i++; - } - for (; (uint)i < (uint)length; i++) { - var digit = value[i] - '0'; - - // Check for invalid digit - if (digit is < 0 or > 9) { - result = 0; - return false; - } - - unchecked { - result = (result * 10) + digit; - } - } - if (isNegative) { - result *= -1; - } - return true; - } - - /// - /// A more convenient way to use - /// - /// - /// - /// - /// The advantage of Concat over string interpolation diminishes when more than 2 strings are used. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Concat(this string value, ReadOnlySpan suffix) => string.Concat(value.AsSpan(), suffix); - /// /// Method used to turn into Title format /// diff --git a/src/Sharpify/Synchronized{T}.cs b/src/Sharpify/Synchronized{T}.cs new file mode 100644 index 0000000..08d7d36 --- /dev/null +++ b/src/Sharpify/Synchronized{T}.cs @@ -0,0 +1,32 @@ +namespace Sharpify; + +/// +/// Provides a wrapper around that allows thread-safe access and update, with an optional callback function whenever the value is updated. +/// +/// +public sealed class Synchronized { + private readonly Action? _onUpdate; + + /// + /// The value held by this reference of , get and set are both thread-safe. + /// + public T Value { + get { + return field; + } + set { + Interlocked.Exchange(ref field, value); + if (_onUpdate is not null) _onUpdate(field); + } + } + + /// + /// Creates a new instance of with an and optional callback. + /// + /// The initial value to used. + /// The action that will be executed when the value is updated. + public Synchronized(T initialValue, Action? onUpdate = null) { + Value = initialValue; + _onUpdate = onUpdate; + } +} \ No newline at end of file diff --git a/src/Sharpify/ThreadSafe.cs b/src/Sharpify/ThreadSafe.cs deleted file mode 100644 index d8d3bf3..0000000 --- a/src/Sharpify/ThreadSafe.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Sharpify; - -/// -/// A wrapper around a value that makes it thread safe. -/// -public sealed class ThreadSafe : IEquatable, IEquatable> { - //TODO: Switch to NET9 new Lock type -#if NET9_0_OR_GREATER - private readonly Lock _lock = new(); -#elif NET8_0 - private readonly object _lock = new(); -#endif - private T _value; - - /// - /// Creates a new instance of ThreadSafe with an initial value. - /// - public ThreadSafe(T value) { - _value = value; - } - - /// - /// Creates a new instance of ThreadSafe with the default value of T. - /// - public ThreadSafe() : this(default!) { } - - /// - /// A public getter and setter for the value. - /// - /// - /// The inner operation are thread-safe, use this to change or access the value. - /// - public T Value { - get { - lock (_lock) { - return _value; - } - } - } - - /// - /// Provides a thread-safe way to modify the value. - /// - /// The value after the modification - public T Modify(Func modificationFunc) { - lock (_lock) { - _value = modificationFunc(_value); - return _value; - } - } - - /// - /// Checks if the value is equal to the other value. - /// - /// - /// - public bool Equals(ThreadSafe? other) => other is not null && Equals(other.Value); - - /// - /// Checks if the value is equal to the other value. - /// - /// - /// - public bool Equals(T? other) { - if (other is null) { - return false; - } - lock (_lock) { - return _value is not null && _value.Equals(other); - } - } - - /// - /// Checks if the value is equal to the other value. - /// - /// - /// - public override bool Equals(object? obj) { - return Equals(obj as ThreadSafe); - } - - /// - /// Gets the hash code of the value. - /// - /// - public override int GetHashCode() => Value!.GetHashCode(); - -} \ No newline at end of file diff --git a/src/Sharpify/UnmanangedExtensions.cs b/src/Sharpify/UnmanangedExtensions.cs deleted file mode 100644 index 1e124be..0000000 --- a/src/Sharpify/UnmanangedExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Sharpify; - -public static partial class Extensions { - /// - /// Tries to parse an enum result from a string - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryParseAsEnum( - this string value, - out TEnum result) where TEnum : struct, Enum { - return Enum.TryParse(value, out result) && Enum.IsDefined(result); - } -} \ No newline at end of file diff --git a/src/Sharpify/UnsafeSpanAccessor.cs b/src/Sharpify/UnsafeSpanAccessor.cs index a1d8969..b158833 100644 --- a/src/Sharpify/UnsafeSpanAccessor.cs +++ b/src/Sharpify/UnsafeSpanAccessor.cs @@ -11,27 +11,28 @@ namespace Sharpify; /// /// Only use it where you can guarantee the scope of the span, it is named "Unsafe" for a reason. /// -public unsafe readonly struct UnsafeSpanIterator : IEnumerable -{ +public unsafe readonly struct UnsafeSpanIterator : IEnumerable { private readonly void* _pointer; /// /// The length of the span /// +#pragma warning disable CA1051 // Do not declare visible instance fields + public readonly int Length; +#pragma warning restore CA1051 // Do not declare visible instance fields + /// /// Creates a new instance of over the specified span. /// /// - public UnsafeSpanIterator(ReadOnlySpan span) - { + public UnsafeSpanIterator(ReadOnlySpan span) { _pointer = Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)); Length = span.Length; } - private UnsafeSpanIterator(void* start, int length) - { + private UnsafeSpanIterator(void* start, int length) { _pointer = start; Length = length; } @@ -42,8 +43,7 @@ private UnsafeSpanIterator(void* start, int length) /// /// /// - public UnsafeSpanIterator Slice(int start, int length) - { + public UnsafeSpanIterator Slice(int start, int length) { ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, Length); return new UnsafeSpanIterator(Unsafe.Add(_pointer, start), length); } @@ -53,10 +53,8 @@ public UnsafeSpanIterator Slice(int start, int length) /// /// /// - public ref readonly T this[int index] - { - get - { + public ref readonly T this[int index] { + get { ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Length); void* item = Unsafe.Add(_pointer, index); return ref Unsafe.AsRef(item); @@ -67,10 +65,8 @@ public ref readonly T this[int index] /// Generates an IEnumerable of the elements in the span /// /// - public IEnumerable ToEnumerable() - { - for (var i = 0; i < Length; i++) - { + public IEnumerable ToEnumerable() { + for (var i = 0; i < Length; i++) { yield return this[i]; } } @@ -83,27 +79,23 @@ public IEnumerable ToEnumerable() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - internal struct UnsafeSpanIteratorEnumerator : IEnumerator - { + internal struct UnsafeSpanIteratorEnumerator : IEnumerator { private readonly UnsafeSpanIterator _source; private int _index; private T? _current; - internal UnsafeSpanIteratorEnumerator(UnsafeSpanIterator source) - { + internal UnsafeSpanIteratorEnumerator(UnsafeSpanIterator source) { _source = source; _index = 0; _current = default; } - public void Dispose() {} + public void Dispose() { } - public bool MoveNext() - { + public bool MoveNext() { UnsafeSpanIterator local = _source; - if ((uint)_index < (uint)local.Length) - { + if ((uint)_index < (uint)local.Length) { _current = local[_index]; _index++; return true; @@ -111,8 +103,7 @@ public bool MoveNext() return MoveNextRare(); } - private bool MoveNextRare() - { + private bool MoveNextRare() { _index = _source.Length + 1; _current = default; return false; @@ -120,20 +111,16 @@ private bool MoveNextRare() public readonly T Current => _current!; - readonly object? IEnumerator.Current - { - get - { - if ((uint)_index >= _source.Length + 1) - { + readonly object? IEnumerator.Current { + get { + if ((uint)_index >= _source.Length + 1) { throw new InvalidOperationException("The enumerator has not been started or has already finished."); } return Current; } } - void IEnumerator.Reset() - { + void IEnumerator.Reset() { _index = 0; _current = default; } diff --git a/src/Sharpify/UtilsDateAndTime.cs b/src/Sharpify/UtilsDateAndTime.cs index 7925d01..380e210 100644 --- a/src/Sharpify/UtilsDateAndTime.cs +++ b/src/Sharpify/UtilsDateAndTime.cs @@ -1,145 +1,41 @@ -using System.Buffers; using System.Globalization; -using Sharpify.Collections; - namespace Sharpify; public static partial class Utils { /// - /// Provides utility methods for - /// - public static class DateAndTime { - /// - /// Returns a of the current time - /// - /// - /// This is useful for firing off this task and awaiting it later, because actually takes quite a bit of time - /// - public static ValueTask GetCurrentTimeAsync() => ValueTask.FromResult(DateTime.Now); - - /// - /// Returns a of the current time in binary - /// - /// - /// This is useful for firing off this task and awaiting it later, because actually takes quite a bit of time - /// - public static ValueTask GetCurrentTimeInBinaryAsync() => ValueTask.FromResult(DateTime.Now.ToBinary()); - - private const int TimeSpanRequiredBufferLength = 30; + /// A format for timestamps + /// + public const string TimeStampFormat = "HHmm-dd-MMM-yy"; - /// - /// Formats a to a pretty string, with 2 sections, e.g., "12:34hr" or "05:12d" or "500ms" etc... - /// - /// The TimeSpan to format - /// The Buffer to use - /// - /// Ensure capacity >= 30 - /// - /// part of the written buffer - public static ReadOnlySpan FormatTimeSpan(TimeSpan timeSpan, Span buffer) { - var sb = StringBuffer.Create(buffer); - switch (timeSpan.TotalSeconds) { - case < 1: // Milliseconds: e.g., "500ms" - sb.Append(timeSpan.Milliseconds); - sb.Append("ms"); - break; - case < 60: - // Seconds:Milliseconds: e.g., "03:05s" - if (timeSpan.Seconds < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Seconds); - sb.Append(':'); - if (timeSpan.Milliseconds < 100) { - sb.Append('0'); - if (timeSpan.Milliseconds < 10) { - sb.Append('0'); - } - } - sb.Append(timeSpan.Milliseconds); - sb.Append("s"); - break; - case < 3600: - // Minutes:Seconds: e.g., "01:30m" - if (timeSpan.Minutes < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Minutes); - sb.Append(':'); - if (timeSpan.Seconds < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Seconds); - sb.Append("m"); - break; - case < 86400: - // Hours:Minutes e.g., "12:34hr" - if (timeSpan.Hours < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Hours); - sb.Append(':'); - if (timeSpan.Minutes < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Minutes); - sb.Append("hr"); - break; - default: - // Days:Hours e.g., "05:12d" - if (timeSpan.Days < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Days); - sb.Append(':'); - if (timeSpan.Hours < 10) { - sb.Append('0'); - } - sb.Append(timeSpan.Hours); - sb.Append("d"); - break; - } - - return sb.WrittenSpan; - } - - /// - /// Formats a to a pretty string, with 2 sections, e.g., "12:34hr" or "05:12d" or "500ms" etc... - /// - /// The TimeSpan to format - /// a string representing the TimeSpan - public static string FormatTimeSpan(TimeSpan timeSpan) { - using var owner = MemoryPool.Shared.Rent(TimeSpanRequiredBufferLength); - return new string(FormatTimeSpan(timeSpan, owner.Memory.Span)); + /// + /// Returns a slice over formatted as + /// + /// + /// Ensure capacity >= 30 + /// + public static ReadOnlySpan FormatTimeStamp(DateTime time, Span buffer) { + if (!time.TryFormat(buffer, out int written, TimeStampFormat, CultureInfo.CurrentCulture)) { + return ReadOnlySpan.Empty; } + return buffer.Slice(0, written); + } - /// - /// Returns a Time Stamp (HHMM-dd-mmm-yy) formatted into an existing buffer - /// - /// - /// - /// Ensure capacity >= 30 - /// - public static ReadOnlySpan FormatTimeStamp(DateTime time, Span buffer) { - return StringBuffer.Create(buffer) - .Append(time.Hour) - .Append(time.Minute) - .Append('-') - .Append(time.Day) - .Append('-') - .Append(CultureInfo.CurrentCulture.DateTimeFormat.GetAbbreviatedMonthName(time.Month)) - .Append('-') - .Append(time.Year % 100) - .WrittenSpan; - } + /// + /// Returns formatted as + /// + public static string FormatTimeStamp(DateTime time) => time.ToString(TimeStampFormat, CultureInfo.CurrentCulture); - /// - /// Returns a Time Stamp -> HHMM-dd-mmm-yy - /// - public static string FormatTimeStamp(DateTime time) { - using var owner = MemoryPool.Shared.Rent(TimeSpanRequiredBufferLength); - return new string(FormatTimeStamp(time, owner.Memory.Span)); - } + /// + /// Returns a of the remaining time based on and . + /// + /// Must be between 0 and 1 (inclusive) + /// The time elapsed so far + /// + public static TimeSpan GetRemainingTime(double currentPercentage, TimeSpan elapsed) { + if (currentPercentage >= 1) return TimeSpan.Zero; + if (currentPercentage <= 0) return TimeSpan.MaxValue; + var rem = (1 - currentPercentage) / currentPercentage; + return rem * elapsed; } } \ No newline at end of file diff --git a/src/Sharpify/UtilsEnv.cs b/src/Sharpify/UtilsEnv.cs index e929121..c84ba1c 100644 --- a/src/Sharpify/UtilsEnv.cs +++ b/src/Sharpify/UtilsEnv.cs @@ -1,86 +1,37 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Security.Principal; namespace Sharpify; public static partial class Utils { /// - /// Provides utility methods for + /// Opens the specified URL in the default web browser based on the operating system. /// - public static class Env { - /// - /// Checks if the application is running on Windows. - /// - public static bool IsRunningOnWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - /// - /// Checks if the application is running with administrator privileges. - /// - /// - /// On platforms other than Windows, it returns automatically. - /// - public static bool IsRunningAsAdmin() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return false; - } - - using var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); + /// The URL to open. + /// + /// Currently only Windows, Linux and Mac are supported. + /// + [SupportedOSPlatform("Windows")] + [SupportedOSPlatform("Linux")] + [SupportedOSPlatform("MacOS")] + public static void OpenLink(string url) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + var processInfo = new ProcessStartInfo { + FileName = url, + UseShellExecute = true + }; + using var process = Process.Start(processInfo); + return; } - - /// - /// Returns the base directory of the application. - /// - /// - /// This is tested and works on Windows - /// This is not tested on Linux and Mac but should work - /// Do not use in .NET Maui, it has a special api for this. - /// - public static string GetBaseDirectory() => AppDomain.CurrentDomain.BaseDirectory; - - /// - /// Combines the base directory path with the specified filename. - /// - /// The name of the file. - /// The combined path. - public static string PathInBaseDirectory(ReadOnlySpan filename) => Path.Join(GetBaseDirectory(), filename); - - /// - /// Checks whether Internet connection is available - /// - public static bool IsInternetAvailable => System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable(); - - /// - /// Opens the specified URL in the default web browser based on the operating system. - /// - /// The URL to open. - /// - /// Currently only Windows, Linux and Mac are supported. - /// - [SupportedOSPlatform("Windows")] - [SupportedOSPlatform("Linux")] - [SupportedOSPlatform("MacOS")] - public static void OpenLink(string url) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var processInfo = new ProcessStartInfo { - FileName = url, - UseShellExecute = true - }; - using var process = Process.Start(processInfo); - return; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - using var process = Process.Start("x-www-browser", url); - return; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - using var process = Process.Start("open", url); - return; - } - throw new PlatformNotSupportedException(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + using var process = Process.Start("x-www-browser", url); + return; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + using var process = Process.Start("open", url); + return; } + throw new PlatformNotSupportedException(); } } \ No newline at end of file diff --git a/src/Sharpify/UtilsMathematics.cs b/src/Sharpify/UtilsMathematics.cs index c09e191..ea62fc9 100644 --- a/src/Sharpify/UtilsMathematics.cs +++ b/src/Sharpify/UtilsMathematics.cs @@ -2,58 +2,53 @@ namespace Sharpify; public static partial class Utils { /// - /// Provides utility methods for + /// Returns a rolling average /// - public static class Mathematics { - /// - /// Returns a rolling average - /// - /// The previous average value - /// The new statistic - /// The number of total samples, previous + 1 - /// - /// If the is less or equal to 0, the is returned. - /// A message will be displayed during debug if that happens. - /// An exception will not be thrown at runtime to increase performance. - /// - public static double RollingAverage(double oldAverage, double newNumber, int sampleCount) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(sampleCount, 0); - if (sampleCount is 1) { - return newNumber; - } - double denominator = 1d / sampleCount; - return ((oldAverage * (sampleCount - 1)) + newNumber) * denominator; + /// The previous average value + /// The new statistic + /// The number of total samples, previous + 1 + /// + /// If the is less or equal to 0, the is returned. + /// A message will be displayed during debug if that happens. + /// An exception will not be thrown at runtime to increase performance. + /// + public static double RollingAverage(double oldAverage, double newNumber, int sampleCount) { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(sampleCount, 0); + if (sampleCount is 1) { + return newNumber; } + double denominator = 1d / sampleCount; + return ((oldAverage * (sampleCount - 1)) + newNumber) * denominator; + } - /// - /// Returns the factorial result of - /// - /// - /// - /// If the is less or equal to 0, is returned. - /// A message will be displayed during debug if that happens. - /// An exception will not be thrown at runtime to increase performance. - /// - public static double Factorial(double n) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(n, 0); - if (n is <= 2) { - return n; - } - var num = 1d; - for (; n > 1; n--) { - num *= n; - } - return num; + /// + /// Returns the factorial result of + /// + /// + /// + /// If the is less or equal to 0, is returned. + /// A message will be displayed during debug if that happens. + /// An exception will not be thrown at runtime to increase performance. + /// + public static double Factorial(double n) { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(n, 0); + if (n is <= 2) { + return n; } - - /// - /// Returns an estimate of the -th number in the Fibonacci sequence - /// - public static double FibonacciApproximation(int n) { - var sqrt5 = Math.Sqrt(5); - var numerator = Math.Pow(1 + sqrt5, n) - Math.Pow(1 - sqrt5, n); - var denominator = Math.ScaleB(sqrt5, n); - return numerator / denominator; + var num = 1d; + for (; n > 1; n--) { + num *= n; } + return num; + } + + /// + /// Returns an estimate of the -th number in the Fibonacci sequence + /// + public static double FibonacciApproximation(int n) { + var sqrt5 = Math.Sqrt(5); + var numerator = Math.Pow(1 + sqrt5, n) - Math.Pow(1 - sqrt5, n); + var denominator = Math.ScaleB(sqrt5, n); + return numerator / denominator; } } \ No newline at end of file diff --git a/src/Sharpify/UtilsStrings.cs b/src/Sharpify/UtilsStrings.cs deleted file mode 100644 index a55b45d..0000000 --- a/src/Sharpify/UtilsStrings.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Buffers; - -using Sharpify.Collections; - -namespace Sharpify; - -public static partial class Utils { - /// - /// Provides utility methods for - /// - public static class Strings { - private static ReadOnlySpan FileSizeSuffix => new string[] { "B", "KB", "MB", "GB", "TB", "PB" }; - - /// - /// The required length of the buffer to format bytes - /// - /// - /// This is required to handle , usual cases are much smaller, and this internal uses are pooled. - /// - public const int FormatBytesRequiredLength = 512; - private const double FormatBytesKb = 1024d; - private const double FormatBytesDivisor = 1 / FormatBytesKb; - - /// - /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... - /// - /// string - public static string FormatBytes(long bytes) - => FormatBytes((double)bytes); - - /// - /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... - /// - /// string - public static string FormatBytes(double bytes) { - using var owner = MemoryPool.Shared.Rent(FormatBytesRequiredLength); - return new string(FormatBytes(bytes, owner.Memory.Span)); - } - - /// - /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... into the buffer and returns the written span - /// - /// - /// Ensure capacity >= - /// - /// string - public static ReadOnlySpan FormatBytes(double bytes, Span buffer) { - var suffix = 0; - while (suffix < FileSizeSuffix.Length - 1 && bytes >= FormatBytesKb) { - bytes *= FormatBytesDivisor; - suffix++; - } - return StringBuffer.Create(buffer) - .Append(bytes, "#,##0.##") - .Append(' ') - .Append(FileSizeSuffix[suffix]) - .Allocate(); - } - - /// - /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... into the buffer and returns the written span - /// - /// - /// Ensure capacity >= - /// - /// string - public static ReadOnlySpan FormatBytes(long bytes, Span buffer) - => FormatBytes((double)bytes, buffer); - } -} \ No newline at end of file diff --git a/src/Sharpify/UtilsUnsafe.cs b/src/Sharpify/UtilsUnsafe.cs index 42962f6..931d169 100644 --- a/src/Sharpify/UtilsUnsafe.cs +++ b/src/Sharpify/UtilsUnsafe.cs @@ -1,55 +1,49 @@ +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using @unsafe = System.Runtime.CompilerServices.Unsafe; - namespace Sharpify; public static partial class Utils { /// - /// Provides utility unsafe utility methods for utilization of other high performance apis + /// Creates an integer predicate from a given predicate function. /// - public static class Unsafe { - /// - /// Creates an integer predicate from a given predicate function. - /// - /// The type of the input parameter. - /// The predicate function. - /// An integer predicate. 1 if the original predicate would've return true, otherwise 0 - /// - /// This allows usage of a predicate to count elements that match a given condition, using hardware intrinsics to speed up the process. - /// The integer return value allows to use this converted function with IEnumerable{T}.Sum which is a hardware accelerated method, but the result will be identical to calling Count(predicate). - /// - public static Func CreateIntegerPredicate(Func predicate) => - @unsafe.As, Func>(ref predicate); - - /// - /// Converts a read-only span to a mutable span. - /// - /// The type of elements in the span. - /// The read-only span to convert. - /// A mutable span. - public static unsafe Span AsMutableSpan(ReadOnlySpan span) { - ref var p = ref MemoryMarshal.GetReference(span); - void* pointer = @unsafe.AsPointer(ref p); - return new Span(pointer, span.Length); - } + /// The type of the input parameter. + /// The predicate function. + /// An integer predicate. 1 if the original predicate would've return true, otherwise 0 + /// + /// This allows usage of a predicate to count elements that match a given condition, using hardware intrinsics to speed up the process. + /// The integer return value allows to use this converted function with IEnumerable{T}.Sum which is a hardware accelerated method, but the result will be identical to calling Count(predicate). + /// + public static Func CreateIntegerPredicate(Func predicate) => + Unsafe.As, Func>(ref predicate); - /// - /// Attempts to unbox an object to a specified value type. - /// - /// The value type to unbox to. - /// The object to unbox. - /// When this method returns, contains the unboxed value if the unboxing is successful; otherwise, the default value of . - /// true if the unboxing is successful; otherwise, false. - /// Copied from CommunityToolkit.HighPerformance - public static bool TryUnbox(object obj, out T value) where T : struct { - if (obj.GetType() == typeof(T)) { - value = @unsafe.Unbox(obj); - return true; - } + /// + /// Converts a read-only span to a mutable span. + /// + /// The type of elements in the span. + /// The read-only span to convert. + /// A mutable span. + public static unsafe Span AsMutableSpan(ReadOnlySpan span) { + ref var p = ref MemoryMarshal.GetReference(span); + void* pointer = Unsafe.AsPointer(ref p); + return new Span(pointer, span.Length); + } - value = default; - return false; + /// + /// Attempts to unbox an object to a specified value type. + /// + /// The value type to unbox to. + /// The object to unbox. + /// When this method returns, contains the unboxed value if the unboxing is successful; otherwise, the default value of . + /// true if the unboxing is successful; otherwise, false. + /// Copied from CommunityToolkit.HighPerformance + public static bool TryUnbox(object obj, out T value) where T : struct { + if (obj.GetType() == typeof(T)) { + value = Unsafe.Unbox(obj); + return true; } + + value = default; + return false; } } \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/AddCommand.cs b/tests/Sharpify.CommandLineInterface.Tests/AddCommand.cs deleted file mode 100644 index 4d2484b..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/AddCommand.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Sharpify.CommandLineInterface.Tests; - -public sealed class AddCommand : Command { - public override string Name => "add"; - - public override string Description => "Adds 2 numbers."; - - public override string Usage => "add "; - - public override ValueTask ExecuteAsync(Arguments args) { - if (!args.TryGetValue(0, 0, out var number1)) { - return OutputHelper.Return(" not specified", 404, true); - } - if (!args.TryGetValue(1, 0, out var number2)) { - return OutputHelper.Return(" not specified", 404, true); - } - return OutputHelper.Return($"{number1} + {number2} = {number1 + number2}", 0); - } -} - -public sealed class SynchronousAddCommand : SynchronousCommand { - public override string Name => "sadd"; - - public override string Description => "Adds 2 numbers."; - - public override string Usage => "sadd "; - - public override int Execute(Arguments args) { - if (!args.TryGetValue(0, 0, out var number1)) { - Console.WriteLine(" not specified"); - return 404; - } - if (!args.TryGetValue(1, 0, out var number2)) { - Console.WriteLine(" not specified"); - return 404; - } - Console.WriteLine($"{number1} + {number2} = {number1 + number2}"); - return 0; - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/ArgumentsIsolatedTests.cs b/tests/Sharpify.CommandLineInterface.Tests/ArgumentsIsolatedTests.cs deleted file mode 100644 index 455f9d2..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/ArgumentsIsolatedTests.cs +++ /dev/null @@ -1,269 +0,0 @@ -namespace Sharpify.CommandLineInterface.Tests; - -/// -/// This test class specifically tests that arguments are parsed correctly, and more importantly non-positional arguments are not lost during forwarding (Which happens with the cli builder naturally) -/// -public class ArgumentsIsolatedTests { - private static readonly Arguments Args = Parser.ParseArguments( - "command positional --named1 Harold --named2 Finch positional2 --flag --words \"word1|word2\" --numbers \"1|2\"")!; - [Fact] - public void Arguments_Positional_BeforeForwarding() { - // Positional 0 [Command] - Assert.True(Args.TryGetValue(0, out var pos0)); - Assert.Equal("command", pos0); - - // Positional 0 [positional] - Assert.True(Args.TryGetValue(1, out var pos1)); - Assert.Equal("positional", pos1); - - // Positional 0 [positional2] - Assert.True(Args.TryGetValue(2, out var pos2)); - Assert.Equal("positional2", pos2); - } - - [Fact] - public void Arguments_Positional_AfterForwarding() { - var forwarded = Args.ForwardPositionalArguments(); - - // "command" no longer exists - // positional should be at 0, and positional2 at 1. - - // Positional 0 [positional] - Assert.True(forwarded.TryGetValue(0, out var pos0)); - Assert.Equal("positional", pos0); - - // Positional 1 [positional2] - Assert.True(forwarded.TryGetValue(1, out var pos1)); - Assert.Equal("positional2", pos1); - } - - [Fact] - public void Arguments_Named_BeforeForwarding() { - // named1 - Harold - // named2 - Finch - - Assert.True(Args.TryGetValue("named1", out var firstName)); - Assert.Equal("Harold", firstName); - - Assert.True(Args.TryGetValue("named2", out var lastName)); - Assert.Equal("Finch", lastName); - } - - [Fact] - public void Arguments_Named_AfterForwarding() { - var forwarded = Args.ForwardPositionalArguments(); - - // named1 - Harold - // named2 - Finch - - Assert.True(forwarded.TryGetValue("named1", out var firstName)); - Assert.Equal("Harold", firstName); - - Assert.True(forwarded.TryGetValue("named2", out var lastName)); - Assert.Equal("Finch", lastName); - } - - [Fact] - public void Arguments_Flag_BeforeForwarding() { - Assert.True(Args.HasFlag("flag")); - } - - [Fact] - public void Arguments_Flag_AfterForwarding() { - var forwarded = Args.ForwardPositionalArguments(); - Assert.True(forwarded.HasFlag("flag")); - } - - [Fact] - public void Arguments_Contains_Key() { - Assert.True(Args.Contains("named1")); - } - - [Fact] - public void Arguments_Contains_Position() { - Assert.True(Args.Contains(0)); - } - - [Fact] - public void Arguments_Named_Array_String() { - Assert.True(Args.TryGetValues("words", "|", out var words)); - Assert.Equal(["word1", "word2"], words); - } - - [Fact] - public void Arguments_Named_MultipleKeys_Array_String() { - var args = Parser.ParseArguments("command --words word1|word2")!; - Assert.True(args.TryGetValues(["words", "w"], "|", out var words)); - Assert.Equal(["word1", "word2"], words); - args = Parser.ParseArguments("command --w word1|word2")!; - Assert.True(args.TryGetValues(["words", "w"], "|", out var w)); - Assert.Equal(["word1", "word2"], w); - } - - [Fact] - public void Arguments_Named_MultipleKeys_Array_Int() { - var args = Parser.ParseArguments("command --numbers 1|2")!; - Assert.True(args.TryGetValues(["numbers", "n"], "|", out var numbers)); - Assert.Equal([1, 2], numbers); - args = Parser.ParseArguments("command -n 1|2")!; - Assert.True(args.TryGetValues(["numbers", "n"], "|", out var n)); - Assert.Equal([1, 2], n); - } - - [Fact] - public void Arguments_Positional_Array_String() { - var args = Parser.ParseArguments("command q1|q2")!; - Assert.True(args.TryGetValues(1, "|", out var words)); - Assert.Equal(["q1", "q2"], words); - } - - [Fact] - public void Arguments_Named_Array_Int() { - Assert.True(Args.TryGetValues("numbers", "|", out var numbers)); - Assert.Equal([1, 2], numbers); - } - - [Fact] - public void Arguments_Positional_Array_Int() { - var args = Parser.ParseArguments("command 1|2")!; - Assert.True(args.TryGetValues(1, "|", out var numbers)); - Assert.Equal([1, 2], numbers); - } - - [Fact] - public void Arguments_TryGetValue_Named_Int() { - var args = Parser.ParseArguments("command -x 5 -y Hello")!; - Assert.True(args.TryGetValue("x", 0, out int x)); - Assert.Equal(5, x); - Assert.False(args.TryGetValue("y", 0, out int y)); - Assert.Equal(0, y); - } - - [Fact] - public void Arguments_TryGetValue_Named_Double() { - var args = Parser.ParseArguments("command -x 5 -y Hello")!; - Assert.True(args.TryGetValue("x", 0, out double x)); - Assert.Equal(5, x); - Assert.False(args.TryGetValue("y", 0, out double y)); - Assert.Equal(0, y); - } - - [Fact] - public void Arguments_GetValue_Named_Int() { - var args = Parser.ParseArguments("command -x 5 -y Hello")!; - Assert.Equal(5, args.GetValue("x", 0)); - Assert.Equal(0, args.GetValue("y", 0)); - } - - [Fact] - public void Arguments_GetValue_Named_MultipleKeys_Int() { - var args = Parser.ParseArguments("command -x 5 -y Hello")!; - Assert.Equal(5, args.GetValue(["x", "one"], 0)); - args = Parser.ParseArguments("command --one 5 -y Hello")!; - Assert.Equal(5, args.GetValue(["x", "one"], 0)); - } - - [Fact] - public void Arguments_TryGetValue_Positional_Int() { - var args = Parser.ParseArguments("command 5 Hello")!; - Assert.True(args.TryGetValue(1, 0, out double x)); - Assert.Equal(5, x); - Assert.False(args.TryGetValue(2, 0, out double y)); - Assert.Equal(0, y); - } - - [Fact] - public void Arguments_GetValue_Positional_Int() { - var args = Parser.ParseArguments("command 5 Hello")!; - Assert.Equal(5, args.GetValue(1, 0)); - Assert.Equal(0, args.GetValue(2, 0)); - } - - [Fact] - public void Arguments_TryGetEnum_Positional() { - var args = Parser.ParseArguments("command Blue")!; - Assert.True(args.TryGetEnum(1, out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - } - - [Fact] - public void Arguments_TryGetEnum_Positional_IgnoreCase() { - var args = Parser.ParseArguments("command bLue")!; - Assert.True(args.TryGetEnum(1, true, out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - } - - [Fact] - public void Arguments_TryGetEnum_Named() { - var args = Parser.ParseArguments("command --color Blue")!; - Assert.True(args.TryGetEnum("color", out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - } - - [Fact] - public void Arguments_TryGetEnum_Named_IgnoreCase() { - var args = Parser.ParseArguments("command --color bLue")!; - Assert.True(args.TryGetEnum("color", true, out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - } - - [Fact] - public void Arguments_TryGetEnum_Named_MultipleKeys() { - var args = Parser.ParseArguments("command --color Blue")!; - Assert.True(args.TryGetEnum(["color", "c"], out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - args = Parser.ParseArguments("command -c Blue")!; - Assert.True(args.TryGetEnum(["color", "c"], out ConsoleColor c)); - Assert.Equal(ConsoleColor.Blue, c); - } - - [Fact] - public void Arguments_TryGetEnum_Named_MultipleKeys_IgnoreCase() { - var args = Parser.ParseArguments("command --color bLue")!; - Assert.True(args.TryGetEnum(["color", "c"], true, out ConsoleColor color)); - Assert.Equal(ConsoleColor.Blue, color); - args = Parser.ParseArguments("command -c bLue")!; - Assert.True(args.TryGetEnum(["color", "c"], true, out ConsoleColor c)); - Assert.Equal(ConsoleColor.Blue, c); - } - - [Fact] - public void Arguments_GetEnum_Positional() { - var args = Parser.ParseArguments("command Blue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(1, ConsoleColor.Black)); - } - - [Fact] - public void Arguments_GetEnum_Positional_IgnoreCase() { - var args = Parser.ParseArguments("command bLue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(1, ConsoleColor.Black, true)); - } - - [Fact] - public void Arguments_GetEnum_Named() { - var args = Parser.ParseArguments("command --color Blue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum("color", ConsoleColor.Black)); - } - - [Fact] - public void Arguments_GetEnum_Named_IgnoreCase() { - var args = Parser.ParseArguments("command --color bLue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum("color", ConsoleColor.Black, true)); - } - - [Fact] - public void Arguments_GetEnum_Named_MultipleKeys() { - var args = Parser.ParseArguments("command --color Blue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(["color", "c"], ConsoleColor.Black)); - args = Parser.ParseArguments("command -c Blue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(["color", "c"], ConsoleColor.Black)); - } - - [Fact] - public void Arguments_GetEnum_Named_MultipleKeys_IgnoreCase() { - var args = Parser.ParseArguments("command --color bLue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(["color", "c"], ConsoleColor.Black, true)); - args = Parser.ParseArguments("command -c bLue")!; - Assert.Equal(ConsoleColor.Blue, args.GetEnum(["color", "c"], ConsoleColor.Black, true)); - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/CliBuilderTests.cs b/tests/Sharpify.CommandLineInterface.Tests/CliBuilderTests.cs deleted file mode 100644 index 5542b3b..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/CliBuilderTests.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Sharpify.CommandLineInterface.Tests; - -public class CliBuilderTests { - [Fact] - public void Build_WhenEmpty_ReturnsEmpty() { - var action = () => CliRunner.CreateBuilder().Build(); - - Assert.Throws(action); - } - - [Fact] - public void Build_WhenNotEmpty_ReturnsCliRunner() { - var action = () => CliRunner.CreateBuilder().AddCommand(new EchoCommand()).Build(); - - action(); - } - - [Fact] - public void Build_WhenNotEmpty_ReturnsCliRunnerWithCommands() { - var echo = new EchoCommand(); - - var cliRunner = CliRunner.CreateBuilder().AddCommand(echo).Build(); - - Assert.Contains(echo, cliRunner.Commands); - } - - [Fact] - public async Task Runner_WithCustomWriter_OutputsCommandHelpToWriter() { - var echo = new EchoCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .SetOutputWriter(writer) - .Build(); - await cliRunner.RunAsync("echo --help"); - - Assert.Contains("echo ", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriter_SingleCommand_HelpCommand_OutputsAllInfo() { - var single = new SingleCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(single) - .SetOutputWriter(writer) - .WithMetadata(options => { - options.Name = "Single"; - options.Description = "A single command"; - options.Version = "1.0.0"; - options.Author = "David"; - options.License = "MIT"; - }) - .Build(); - await cliRunner.RunAsync("help", false); - - var output = writer.ToString(); - Assert.Contains("Single", output); - Assert.Contains("A single command", output); - Assert.Contains("Version: 1.0.0", output); - Assert.Contains("Author: David", output); - Assert.Contains("License: MIT", output); - Assert.Contains(single.Usage, output); - } - - [Theory] - [InlineData("help")] - [InlineData("--help")] - public async Task Runner_WithCustomWriter_SingleCommand_HelpCommand_OutputsCommandUsageToWriter(string input) { - var single = new SingleCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(single) - .SetOutputWriter(writer) - .Build(); - await cliRunner.RunAsync(input, false); - - Assert.Contains(single.Usage, writer.ToString()); - } - - [Theory] - [InlineData("version")] - [InlineData("--version")] - public async Task Runner_WithCustomWriter_SingleCommand_VersionCommand_OutputsVersionToWriter(string input) { - var single = new SingleCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(single) - .SetOutputWriter(writer) - .WithMetadata(options => options.Version = "1.0.0") - .Build(); - await cliRunner.RunAsync(input, false); - - Assert.Contains("Version: 1.0.0", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterMultipleCommands_OutputsGeneralHelpToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .Build(); - await cliRunner.RunAsync("--help"); - - Assert.Contains("Echo", writer.ToString()); - Assert.Contains("Add", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAddCommand_ReadOnlySpanInput() { - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(add) - .SetOutputWriter(writer) - .Build(); - await cliRunner.RunAsync(["add", "1", "2"]); - - Assert.Contains("3", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAndMetadata_HelpCommand_OutputsGeneralHelpToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .WithMetadata(options => options.Author = "Dave") - .Build(); - await cliRunner.RunAsync("help"); - - Assert.Contains("Dave", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAndMetadata_HelpFlag_OutputsGeneralHelpToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .WithMetadata(options => options.Author = "Dave") - .Build(); - await cliRunner.RunAsync("--help"); - - Assert.Contains("Dave", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAndMetadata_VersionCommand_OutputsVersionToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .WithMetadata(options => { - options.Author = "Dave"; - options.Version = "1.0.0"; - }) - .Build(); - await cliRunner.RunAsync("version"); - - Assert.Contains("Version: 1.0.0", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAndMetadata_VersionFlag_OutputsVersionToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .WithMetadata(options => { - options.Author = "Dave"; - options.Version = "1.0.0"; - }) - .Build(); - await cliRunner.RunAsync("--version"); - - Assert.Contains("Version: 1.0.0", writer.ToString()); - } - - [Fact] - public async Task Runner_WithCustomWriterAndCustomHeader_OutputsGeneralHelpToWriter() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .SetOutputWriter(writer) - .WithCustomHeader("Dave") - .SetHelpTextSource(HelpTextSource.CustomHeader) - .Build(); - await cliRunner.RunAsync("--help"); - - Assert.Contains("Dave", writer.ToString()); - } - - [Fact] - public void Runner_WithOrderedCommands_IsOrdered() { - var echo = new EchoCommand(); - var add = new AddCommand(); - var sAdd = new SynchronousAddCommand(); - var writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(echo) - .AddCommand(add) - .AddCommand(sAdd) - .SortCommandsAlphabetically() - .SetOutputWriter(writer) - .Build(); - var copy = cliRunner.Commands; - Assert.Equal(add, copy[0]); - Assert.Equal(echo, copy[1]); - Assert.Equal(sAdd, copy[2]); - } - - [Fact] - public async Task Runner_WithSingleCommand_NoParams() { - StrongBox value = new(false); - - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(new SingleCommandNoParams(value)) - .ConfigureEmptyInputBehavior(EmptyInputBehavior.AttemptToProceed) - .Build(); - var exitCode = await cliRunner.RunAsync("", false); - - Assert.Equal(0, exitCode); - Assert.True(value.Value); - } - - [Fact] - public async Task Runner_WithMultipleCommands_CaseSensitive() { - var cliRunner = CliRunner.CreateBuilder() - .AddCommand(new AddCommand()) - .AddCommand(new EchoCommand()) - .ConfigureArgumentCaseHandling(ArgumentCaseHandling.CaseSensitive) - .Build(); - var exitCode = await cliRunner.RunAsync("aDD 1 2"); - - Assert.NotEqual(0, exitCode); - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/EchoCommand.cs b/tests/Sharpify.CommandLineInterface.Tests/EchoCommand.cs deleted file mode 100644 index aa9faee..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/EchoCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Sharpify.CommandLineInterface.Tests; - -public sealed class EchoCommand : Command { - public override string Name => "echo"; - - public override string Description => "Echoes the specified message."; - - public override string Usage => "echo "; - - public override ValueTask ExecuteAsync(Arguments args) { - if (!args.TryGetValue("message", out string message)) { - return OutputHelper.Return("No message specified", 404, true); - } - return OutputHelper.Return(message, 0); - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/GlobalUsings.cs b/tests/Sharpify.CommandLineInterface.Tests/GlobalUsings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/Helper.cs b/tests/Sharpify.CommandLineInterface.Tests/Helper.cs deleted file mode 100644 index 9778dff..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/Helper.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Sharpify.CommandLineInterface.Tests; - -public static class Helper { - public static Dictionary GetMapped(params (string, string)[] parameters) { - var dict = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (var (key, value) in parameters) { - dict[key] = value; - } - return dict; - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/ParserArgumentsTests.cs b/tests/Sharpify.CommandLineInterface.Tests/ParserArgumentsTests.cs deleted file mode 100644 index ba5749e..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/ParserArgumentsTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Sharpify.CommandLineInterface.Tests; - -public class ParserArgumentsTests { - [Fact] - public void Split_WhenEmpty_ReturnsEmptyList() { - Assert.Empty(Parser.Split("")); - } - - [Theory] - [InlineData("hello", new[] { "hello" })] - [InlineData("hello world", new[] { "hello", "world" })] - [InlineData("\"hello world\"", new[] { "hello world" })] - [InlineData("\"hello world\" \"hello world\"", new[] { "hello world", "hello world" })] - public void Split_WhenValid_ReturnsValid(string input, string[] expected) { - Assert.Equal(expected, Parser.Split(input)); - } - - [Fact] - public void MapArguments_Valid() { - var args = new string[][] { - ["command", "--message", "hello world", "--code", "404", "--force"], // combined - ["command", "--m", "hello world", "--c", "404", "--force"], // named + switch - ["command", "-m", "hello world", "-c", "404", "--force"], // short + switch - ["command", "--attribute", "hidden", "--file", "file.txt"], // combined - ["command", "--a", "hidden", "--f", "file.txt"], // named - ["do-this", "--n", "name", "--f", "file1.txt file2.txt"], // named - ["test", "one", "--param", "value", "two"], // positional after named - }; - var expected = new Dictionary[] { - Helper.GetMapped(("0", "command"), ("message", "hello world"), ("code", "404"), ("force", "")), - Helper.GetMapped(("0", "command"), ("m", "hello world"), ("c", "404"), ("force", "")), - Helper.GetMapped(("0", "command"), ("m", "hello world"), ("c", "404"), ("force", "")), - Helper.GetMapped(("0", "command"), ("attribute", "hidden"), ("file", "file.txt")), - Helper.GetMapped(("0", "command"), ("a", "hidden"), ("f", "file.txt")), - Helper.GetMapped(("0", "do-this"), ("n", "name"), ("f", "file1.txt file2.txt")), - Helper.GetMapped(("0", "test"), ("1", "one"), ("param", "value"), ("2", "two")), - }; - for (var i = 0; i < args.Length; i++) { - var localArgs = args[i].AsReadOnly(); - var localArguments = Parser.MapArguments(localArgs, StringComparer.CurrentCultureIgnoreCase); - Assert.Equal(expected[i], localArguments); - } - } - - [Fact] - public void Parse_WhenEmpty_ReturnsValidButEmptyArguments() { - Assert.Equal(0, Parser.ParseArguments("").Count); - } - - [Fact] - public void ParseArguments_ForCollection_List() { - List args = ["command", "--message", "hello world", "--code", "404", "--force"]; - var arguments = Parser.ParseArguments(args, StringComparer.OrdinalIgnoreCase); - Assert.NotNull(arguments); - Assert.Equal(4, arguments.Count); - Assert.Equal("command", arguments.GetValue(0, "")); - Assert.Equal("hello world", arguments.GetValue("message", "")); - Assert.Equal(404, arguments.GetValue("code", 0)); - Assert.True(arguments.HasFlag("force")); - } - - [Fact] - public void ParseArguments_ForCollection_Array() { - string[] args = ["command", "--message", "hello world", "--code", "404", "--force"]; - var arguments = Parser.ParseArguments(args, StringComparer.OrdinalIgnoreCase); - Assert.NotNull(arguments); - Assert.Equal(4, arguments.Count); - Assert.Equal("command", arguments.GetValue(0, "")); - Assert.Equal("hello world", arguments.GetValue("message", "")); - Assert.Equal(404, arguments.GetValue("code", 0)); - Assert.True(arguments.HasFlag("force")); - } - - [Fact] - public void ParseArguments_ForCollection_ReadOnlyCollection() { - ReadOnlyCollection roc = new(["command", "--message", "hello world", "--code", "404", "--force"]); - var arguments = Parser.ParseArguments(roc, StringComparer.OrdinalIgnoreCase); - Assert.NotNull(arguments); - Assert.Equal(4, arguments.Count); - Assert.Equal("command", arguments.GetValue(0, "")); - Assert.Equal("hello world", arguments.GetValue("message", "")); - Assert.Equal(404, arguments.GetValue("code", 0)); - Assert.True(arguments.HasFlag("force")); - } - - [Fact] - public void Parse_And_Arguments_Command_Name() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue(0, out var command)); - Assert.Equal("command", command); - } - - [Fact] - public void Parse_And_Arguments_Named_Argument() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue("message", out var message)); - Assert.Equal("hello world", message); - } - - [Fact] - public void Parse_And_Arguments_Named_Argument_Multiple() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValues("message", " ", out var message)); - Assert.Equal(["hello", "world"], message); - } - - [Fact] - public void Parse_And_Arguments_Named_Argument_With_Aliases() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue(["message", "m"], out var message)); - Assert.Equal("hello world", message); - } - - [Fact] - public void Parse_And_Arguments_Named_Argument_With_Aliases_Inverted() { - const string input = "command -m \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue(["message", "m"], out var message)); - Assert.Equal("hello world", message); - } - - [Fact] - public void Parse_And_Arguments_Named_Argument_Integer_WithDefault() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue("code", 12, out var code)); - Assert.Equal(404, code); - } - - [Fact] - public void Parse_And_Arguments_With_Flag() { - const string input = "command --message \"hello world\" --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.HasFlag("force")); - } - - [Fact] - public void Parse_And_Arguments_Positional_Negative_Numeric() { - const string input = "command -5 -9"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - Assert.True(arguments!.TryGetValue(1, 0, out int num)); - Assert.Equal(-5, num); - Assert.True(arguments!.TryGetValue(2, 0, out int num2)); - Assert.Equal(-9, num2); - } - - [Fact] - public void Arguments_ForwardPositional_Works() { - const string input = "command delete --code 404 --force"; - var arguments = Parser.ParseArguments(input); - Assert.NotNull(arguments); - var containsCommandAtPosition0 = arguments!.TryGetValue(0, out var command); - Assert.True(containsCommandAtPosition0); - Assert.Equal("command", command); - var containsDelete = arguments.TryGetValue(1, out var delete); - Assert.True(containsDelete); - Assert.Equal("delete", delete); - var forwarded = arguments.ForwardPositionalArguments(); - Assert.NotNull(forwarded); - var first = forwarded!.TryGetValue(0, out var firstArg); - Assert.True(first); - Assert.Equal("delete", firstArg); - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj b/tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj deleted file mode 100644 index 1c472a6..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net9.0 - true - true - enable - enable - Exe - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/SingleCommand.cs b/tests/Sharpify.CommandLineInterface.Tests/SingleCommand.cs deleted file mode 100644 index ddae9ea..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/SingleCommand.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Sharpify.CommandLineInterface.Tests; - -public sealed class SingleCommand : Command { - public override string Name => ""; - - public override string Description => "Echoes the specified message."; - - public override string Usage => "Single "; - - public override ValueTask ExecuteAsync(Arguments args) { - if (!args.TryGetValue("message", out string message)) { - return OutputHelper.Return("No message specified", 404, true); - } - return OutputHelper.Return(message, 0); - } -} - -public sealed class SingleCommandNoParams : SynchronousCommand { - public override string Name => ""; - - public override string Description => "Changes the inner boxed value to true."; - - public override string Usage => ""; - - private readonly StrongBox _value; - - public SingleCommandNoParams(StrongBox value) { - _value = value; - } - - public override int Execute(Arguments args) { - _value.Value = true; - return 0; - } -} \ No newline at end of file diff --git a/tests/Sharpify.CommandLineInterface.Tests/xunit.runner.json b/tests/Sharpify.CommandLineInterface.Tests/xunit.runner.json deleted file mode 100644 index 7d6ce78..0000000 --- a/tests/Sharpify.CommandLineInterface.Tests/xunit.runner.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "diagnosticMessages": true, - "parallelizeAssembly": false, - "parallelizeTestCollections": false, - "showLiveOutput": true -} diff --git a/tests/Sharpify.Data.Tests/AssemblyInfo.cs b/tests/Sharpify.Data.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa..0000000 --- a/tests/Sharpify.Data.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/Color.cs b/tests/Sharpify.Data.Tests/Color.cs deleted file mode 100644 index caea671..0000000 --- a/tests/Sharpify.Data.Tests/Color.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MemoryPack; - -namespace Sharpify.Data.Tests; - -public record Color { - public string Name { get; set; } = ""; - public byte Red { get; set; } - public byte Green { get; set; } - public byte Blue { get; set; } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/DatabaseEncryptedIgnoreCaseTests.cs b/tests/Sharpify.Data.Tests/DatabaseEncryptedIgnoreCaseTests.cs deleted file mode 100644 index 5216fe3..0000000 --- a/tests/Sharpify.Data.Tests/DatabaseEncryptedIgnoreCaseTests.cs +++ /dev/null @@ -1,369 +0,0 @@ -namespace Sharpify.Data.Tests; - -public class DatabaseEncryptedIgnoreCaseTests { - private static Func> Factory => p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = Database.CreateOrLoad(new() { - Path = path, - EncryptionKey = "test", - IgnoreCase = true, - SerializeOnUpdate = true, - TriggerUpdateEvents = true, - }); - return new(path, database); - }; - - private static Func>> AsyncFactory => async p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = await Database.CreateOrLoadAsync(new() { - Path = path, - IgnoreCase = true, - SerializeOnUpdate = false, - TriggerUpdateEvents = false, - }); - return new(path, database); - }; - - [Fact] - public void SerializeAndDeserialize() { - using var database = Database.CreateOrLoad(new() { - Path = Path.GetTempFileName(), - EncryptionKey = "test", - IgnoreCase = true, - }); - - database.Upsert("test", new Person("David", 27)); - database.Serialize(); - var length = new FileInfo(database.Config.Path).Length; - - using var database2 = Database.CreateOrLoad(new() { - Path = database.Config.Path, - EncryptionKey = "test", - IgnoreCase = true, - }); - - Assert.True(database2.TryGetValue("TEST", out Person result)); - Assert.Equal(new Person("David", 27), result); - } - - [Fact] - public async Task AsyncSerializeDeserialize() { - // Arrange - using var db = await AsyncFactory(""); - - // Act - db.Database.Upsert("test", new Person("David", 27)); - - await db.Database.SerializeAsync(); - - // Arrange - using var db2 = await AsyncFactory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("TEST", out Person result)); - Assert.Equal(new Person("David", 27), result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Upsert() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("TEST", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertEncrypted() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test", "enc"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("TEST", "enc", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertBytes() { - // Arrange - using var db = Factory(""); - - // Act - var bytes = new byte[] { 1, 2, 3, 4, 5 }; - db.Database.Upsert("test", bytes); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("TEST", out var result)); - Assert.Equal(bytes, result.Span); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMemoryPackable() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - db.Database.Upsert("1", p1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMany() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - db.Database.UpsertMany("1", [p1, p2]); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertJson() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Color { - Name = "Red", - Red = 255, - Green = 0, - Blue = 0 - }; - db.Database.Upsert("1", p1, JsonContext.Default.Color); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", JsonContext.Default.Color, out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void GeneralFilterTest() { - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var d1 = new Dog("Buddy", 5); - - db.Database.CreateMemoryPackFilter().Upsert("David", p1); - db.Database.CreateMemoryPackFilter().Upsert("Buddy", d1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.False(db2.Database.ContainsKey("David")); - Assert.False(db2.Database.ContainsKey("Buddy")); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("DAVID", out var p2)); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("BUDDY", out var d2)); - Assert.Equal(p1, p2); - Assert.Equal(d1, d2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public async Task UpsertConcurrently() { - // Arrange - using var db = Factory(""); - - // Act - var items = Enumerable.Range(0, 100).ToArray(); - var test = new ConcurrentTest(db.Database); - await items.ForAllAsync(test); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.Equal(100, db2.Database.Count); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Contains() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - - // Assert - Assert.True(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void ContainsFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - - // Assert - Assert.True(db.Database.CreateMemoryPackFilter().ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Remove() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove("test"); - - // Assert - Assert.False(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemovePredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove("test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFilteredPredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Clear() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Clear(); - - // Assert - Assert.False(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - private class ConcurrentTest : IAsyncAction { - private readonly Database _database; - - public ConcurrentTest(Database database) { - _database = database; - } - - public Task InvokeAsync(int item, CancellationToken token = default) { - var rnd = Random.Shared.Next(10_000, 200_000); - _database.Upsert(item.ToString(), rnd.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/DatabaseEncryptedTests.cs b/tests/Sharpify.Data.Tests/DatabaseEncryptedTests.cs deleted file mode 100644 index 2946a65..0000000 --- a/tests/Sharpify.Data.Tests/DatabaseEncryptedTests.cs +++ /dev/null @@ -1,365 +0,0 @@ -namespace Sharpify.Data.Tests; - -public class DatabaseEncryptedTests { - private static Func> Factory => p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = Database.CreateOrLoad(new() { - Path = path, - EncryptionKey = "test", - SerializeOnUpdate = true, - TriggerUpdateEvents = true, - }); - return new(path, database); - }; - - private static Func>> AsyncFactory => async p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = await Database.CreateOrLoadAsync(new() { - Path = path, - SerializeOnUpdate = false, - TriggerUpdateEvents = false, - }); - return new(path, database); - }; - - [Fact] - public void SerializeAndDeserialize() { - using var database = Database.CreateOrLoad(new() { - Path = Path.GetTempFileName(), - EncryptionKey = "test" - }); - - database.Upsert("test", new Person("David", 27)); - database.Serialize(); - var length = new FileInfo(database.Config.Path).Length; - - using var database2 = Database.CreateOrLoad(new() { - Path = database.Config.Path, - EncryptionKey = "test" - }); - - Assert.True(database2.TryGetValue("test", out Person result)); - Assert.Equal(new Person("David", 27), result); - } - - [Fact] - public async Task AsyncSerializeDeserialize() { - // Arrange - using var db = await AsyncFactory(""); - - // Act - db.Database.Upsert("test", new Person("David", 27)); - - await db.Database.SerializeAsync(); - - // Arrange - using var db2 = await AsyncFactory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("test", out Person result)); - Assert.Equal(new Person("David", 27), result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Upsert() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("test", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertEncrypted() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test", "enc"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("test", "enc", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertBytes() { - // Arrange - using var db = Factory(""); - - // Act - byte[] bytes = [1, 2, 3, 4, 5]; - db.Database.Upsert("test", bytes); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("test", out var result)); - Assert.Equal(bytes, result.Span); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMemoryPackable() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - db.Database.Upsert("1", p1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMany() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - db.Database.UpsertMany("1", [p1, p2]); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertJson() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Color { - Name = "Red", - Red = 255, - Green = 0, - Blue = 0 - }; - db.Database.Upsert("1", p1, JsonContext.Default.Color); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", JsonContext.Default.Color, out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void GeneralFilterTest() { - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var d1 = new Dog("Buddy", 5); - - db.Database.CreateMemoryPackFilter().Upsert("David", p1); - db.Database.CreateMemoryPackFilter().Upsert("Buddy", d1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.False(db2.Database.ContainsKey("David")); - Assert.False(db2.Database.ContainsKey("Buddy")); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("David", out var p2)); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("Buddy", out var d2)); - Assert.Equal(p1, p2); - Assert.Equal(d1, d2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public async Task UpsertConcurrently() { - // Arrange - using var db = Factory(""); - - // Act - var items = Enumerable.Range(0, 100).ToArray(); - var test = new ConcurrentTest(db.Database); - await items.ForAllAsync(test); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.Equal(100, db2.Database.Count); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Contains() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - - // Assert - Assert.True(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void ContainsFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - - // Assert - Assert.True(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Remove() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove("test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemovePredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove("test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFilteredPredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Clear() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Clear(); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - private class ConcurrentTest : IAsyncAction { - private readonly Database _database; - - public ConcurrentTest(Database database) { - _database = database; - } - - public Task InvokeAsync(int item, CancellationToken token = default) { - var rnd = Random.Shared.Next(10_000, 200_000); - _database.Upsert(item.ToString(), rnd.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/DatabaseIgnoreCaseTests.cs b/tests/Sharpify.Data.Tests/DatabaseIgnoreCaseTests.cs deleted file mode 100644 index 607230e..0000000 --- a/tests/Sharpify.Data.Tests/DatabaseIgnoreCaseTests.cs +++ /dev/null @@ -1,368 +0,0 @@ -namespace Sharpify.Data.Tests; - -public class DatabaseIgnoreCaseTests { - private static Func> Factory => p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = Database.CreateOrLoad(new() { - Path = path, - IgnoreCase = true, - SerializeOnUpdate = true, - TriggerUpdateEvents = true, - }); - return new(path, database); - }; - - private static Func>> AsyncFactory => async p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = await Database.CreateOrLoadAsync(new() { - Path = path, - IgnoreCase = true, - SerializeOnUpdate = false, - TriggerUpdateEvents = false, - }); - return new(path, database); - }; - - [Fact] - public void SerializeAndDeserialize() { - using var database = Database.CreateOrLoad(new() { - Path = Path.GetTempFileName(), - EncryptionKey = "test", - IgnoreCase = true, - }); - - database.Upsert("test", new Person("David", 27)); - database.Serialize(); - var length = new FileInfo(database.Config.Path).Length; - - using var database2 = Database.CreateOrLoad(new() { - Path = database.Config.Path, - EncryptionKey = "test", - IgnoreCase = true, - }); - - Assert.True(database2.TryGetValue("TEST", out Person result)); - Assert.Equal(new Person("David", 27), result); - } - - [Fact] - public async Task AsyncSerializeDeserialize() { - // Arrange - using var db = await AsyncFactory(""); - - // Act - db.Database.Upsert("test", new Person("David", 27)); - - await db.Database.SerializeAsync(); - - // Arrange - using var db2 = await AsyncFactory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("TEST", out Person result)); - Assert.Equal(new Person("David", 27), result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Upsert() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("TEST", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertEncrypted() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test", "enc"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("TEST", "enc", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertBytes() { - // Arrange - using var db = Factory(""); - - // Act - byte[] bytes = [1, 2, 3, 4, 5]; - db.Database.Upsert("test", bytes); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("TEST", out var result)); - Assert.Equal(bytes, result.Span); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMemoryPackable() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - db.Database.Upsert("1", p1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMany() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - db.Database.UpsertMany("1", [p1, p2]); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertJson() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Color { - Name = "Red", - Red = 255, - Green = 0, - Blue = 0 - }; - db.Database.Upsert("1", p1, JsonContext.Default.Color); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", JsonContext.Default.Color, out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void GeneralFilterTest() { - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var d1 = new Dog("Buddy", 5); - - db.Database.CreateMemoryPackFilter().Upsert("David", p1); - db.Database.CreateMemoryPackFilter().Upsert("Buddy", d1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.False(db2.Database.ContainsKey("David")); - Assert.False(db2.Database.ContainsKey("Buddy")); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("DAVID", out var p2)); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("BUDDY", out var d2)); - Assert.Equal(p1, p2); - Assert.Equal(d1, d2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public async Task UpsertConcurrently() { - // Arrange - using var db = Factory(""); - - // Act - var items = Enumerable.Range(0, 100).ToArray(); - var test = new ConcurrentTest(db.Database); - await items.ForAllAsync(test); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.Equal(100, db2.Database.Count); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Contains() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - - // Assert - Assert.True(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void ContainsFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - - // Assert - Assert.True(db.Database.CreateMemoryPackFilter().ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Remove() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove("test"); - - // Assert - Assert.False(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemovePredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove("test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFilteredPredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Clear() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Clear(); - - // Assert - Assert.False(db.Database.ContainsKey("TEST")); - - // Cleanup - File.Delete(db.Path); - } - - private class ConcurrentTest : IAsyncAction { - private readonly Database _database; - - public ConcurrentTest(Database database) { - _database = database; - } - - public Task InvokeAsync(int item, CancellationToken token = default) { - var rnd = Random.Shared.Next(10_000, 200_000); - _database.Upsert(item.ToString(), rnd.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/DatabaseTests.cs b/tests/Sharpify.Data.Tests/DatabaseTests.cs deleted file mode 100644 index aa88a88..0000000 --- a/tests/Sharpify.Data.Tests/DatabaseTests.cs +++ /dev/null @@ -1,441 +0,0 @@ -using System.Buffers; - -namespace Sharpify.Data.Tests; - -public class DatabaseTests { - private static Func> Factory => p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = Database.CreateOrLoad(new() { - Path = path, - SerializeOnUpdate = true, - TriggerUpdateEvents = true, - }); - return new(path, database); - }; - - private static Func>> AsyncFactory => async p => { - var path = p.Length is 0 ? - Path.GetTempFileName() - : p; - var database = await Database.CreateOrLoadAsync(new() { - Path = path, - SerializeOnUpdate = false, - TriggerUpdateEvents = false, - }); - return new(path, database); - }; - - [Fact] - public void SerializeAndDeserialize() { - using var database = Database.CreateOrLoad(new() { - Path = Path.GetTempFileName(), - EncryptionKey = "test" - }); - - database.Upsert("test", new Person("David", 27)); - database.Serialize(); - var length = new FileInfo(database.Config.Path).Length; - - using var database2 = Database.CreateOrLoad(new() { - Path = database.Config.Path, - EncryptionKey = "test" - }); - - Assert.True(database2.TryGetValue("test", out Person result)); - Assert.Equal(new Person("David", 27), result); - } - - [Fact] - public async Task AsyncSerializeDeserialize() { - // Arrange - using var db = await AsyncFactory(""); - - // Act - db.Database.Upsert("test", new Person("David", 27)); - - await db.Database.SerializeAsync(); - - // Arrange - using var db2 = await AsyncFactory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("test", out Person result)); - Assert.Equal(new Person("David", 27), result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Upsert() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("test", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertEncrypted() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test", "enc"); - db.Database.Serialize(); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetString("test", "enc", out string result)); - Assert.Equal("test", result); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertBytes() { - // Arrange - using var db = Factory(""); - - // Act - byte[] bytes = [1, 2, 3, 4, 5]; - db.Database.Upsert("test", bytes); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("test", out var result)); - Assert.Equal(bytes, result.Span); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void SpanBasedByteReadWrite() { - // Arrange - using var db = Factory(""); - - // Act - byte[] bytes = [1, 2, 3, 4, 5]; - db.Database.Upsert("test", bytes); - - using var buffer = db.Database.TryReadToRentedBuffer("test", "", 1); - buffer.WriteAndAdvance(6); - db.Database.Upsert("test", buffer.WrittenSpan); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("test", out var result)); - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6 }, result.Span); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMemoryPackable() { - // Arrange - using var db = Factory(""); - - var filter = db.Database.CreateMemoryPackFilter(); - - // Act - var p1 = new Person("David", 27); - db.Database.Upsert("1", p1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertMany() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - db.Database.UpsertMany("1", [p1, p2]); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertManySpan() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - Span span = [p1, p2]; - db.Database.UpsertMany("1", span); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void SpanBasedTReadWrite() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var p2 = new Person("John", 30); - var p3 = new Person("Jane", 25); - Span span = [p1, p2]; - db.Database.UpsertMany("1", span); - - using var buffer = db.Database.TryReadToRentedBuffer("1", "", 1); - buffer.WriteAndAdvance(p3); - db.Database.UpsertMany("1", buffer.WrittenSpan); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValues("1", out var arr)); - Assert.Equal([p1, p2, p3], arr); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void UpsertJson() { - // Arrange - using var db = Factory(""); - - // Act - var p1 = new Color { - Name = "Red", - Red = 255, - Green = 0, - Blue = 0 - }; - db.Database.Upsert("1", p1, JsonContext.Default.Color); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.True(db2.Database.TryGetValue("1", JsonContext.Default.Color, out var p2)); - Assert.Equal(p1, p2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void GeneralFilterTest() { - using var db = Factory(""); - - // Act - var p1 = new Person("David", 27); - var d1 = new Dog("Buddy", 5); - - db.Database.CreateMemoryPackFilter().Upsert("David", p1); - db.Database.CreateMemoryPackFilter().Upsert("Buddy", d1); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.False(db2.Database.ContainsKey("David")); - Assert.False(db2.Database.ContainsKey("Buddy")); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("David", out var p2)); - Assert.True(db.Database.CreateMemoryPackFilter().TryGetValue("Buddy", out var d2)); - Assert.Equal(p1, p2); - Assert.Equal(d1, d2); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public async Task UpsertConcurrently() { - // Arrange - using var db = Factory(""); - - // Act - var items = Enumerable.Range(0, 100).ToArray(); - var test = new ConcurrentTest(db.Database); - await items.ForAllAsync(test); - - // Arrange - using var db2 = Factory(db.Path); - - // Assert - Assert.Equal(100, db2.Database.Count); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Contains() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - - // Assert - Assert.True(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void ContainsFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - - // Assert - Assert.True(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Remove() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove("test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemovePredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFiltered() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove("test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void RemoveFilteredPredicate() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.CreateMemoryPackFilter().Upsert("test", new Person("David", 27)); - db.Database.CreateMemoryPackFilter().Remove(key => key == "test"); - - // Assert - Assert.False(db.Database.CreateMemoryPackFilter().ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - [Fact] - public void Clear() { - // Arrange - using var db = Factory(""); - - // Act - db.Database.Upsert("test", "test"); - db.Database.Clear(); - - // Assert - Assert.False(db.Database.ContainsKey("test")); - - // Cleanup - File.Delete(db.Path); - } - - private class ConcurrentTest : IAsyncAction { - private readonly Database _database; - - public ConcurrentTest(Database database) { - _database = database; - } - - public Task InvokeAsync(int item, CancellationToken token = default) { - var rnd = Random.Shared.Next(10_000, 200_000); - _database.Upsert(item.ToString(), rnd.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/Dog.cs b/tests/Sharpify.Data.Tests/Dog.cs deleted file mode 100644 index 71b2790..0000000 --- a/tests/Sharpify.Data.Tests/Dog.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -using MemoryPack; - -namespace Sharpify.Data.Tests; - -[MemoryPackable] -public readonly partial record struct Dog(string Name, int Age); \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/FactoryResult.cs b/tests/Sharpify.Data.Tests/FactoryResult.cs deleted file mode 100644 index 2d7de90..0000000 --- a/tests/Sharpify.Data.Tests/FactoryResult.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Sharpify.Data.Tests; - -public record FactoryResult(string Path, T Database) : IDisposable where T : IDisposable { - public void Dispose() => Database.Dispose(); -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/GlobalUsings.cs b/tests/Sharpify.Data.Tests/GlobalUsings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/tests/Sharpify.Data.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/HelperTests.cs b/tests/Sharpify.Data.Tests/HelperTests.cs deleted file mode 100644 index 77c985e..0000000 --- a/tests/Sharpify.Data.Tests/HelperTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Bogus; - -using MemoryPack; - -using Sharpify.Collections; - -namespace Sharpify.Data.Tests; - -public class HelperTests { - [Theory] - [InlineData(new byte[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new double[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new[] { "1", "2", "3", "4", "5", "6" }, 6)] - public void GetRequiredLength_Unmanaged(T[] data, int expectedLength) { - var serialized = MemoryPackSerializer.Serialize(data); - var requiredLength = Helper.GetRequiredLength(serialized); - Assert.Equal(expectedLength, requiredLength); - } - - [Fact] - public void GetRequiredLength_Person() { - var faker = new Faker(); - var data = Enumerable.Range(1, faker.Random.Int(10, 100)).Select(_ => new Person(faker.Name.FullName(), faker.Random.Int(1, 100))).ToArray(); - var serialized = MemoryPackSerializer.Serialize(data); - var requiredLength = Helper.GetRequiredLength(serialized); - Assert.Equal(data.Length, requiredLength); - } - - [Theory] - [InlineData(new byte[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new double[] { 1, 2, 3, 4, 5, 6 }, 6)] - [InlineData(new[] { "1", "2", "3", "4", "5", "6" }, 6)] - public void ReadToRentedBufferWriter_Unmanaged(T[] data, int expectedLength) { - var serialized = MemoryPackSerializer.Serialize(data); - var requiredLength = Helper.GetRequiredLength(serialized); - var buffer = new RentedBufferWriter(requiredLength + 5); - try { - Helper.ReadToRenterBufferWriter(ref buffer, serialized, requiredLength); - Assert.Equal(expectedLength, buffer.Position); - } finally { - buffer?.Dispose(); - } - } - - [Fact] - public void ReadToRentedBufferWriter_Person() { - var faker = new Faker(); - var data = Enumerable.Range(1, faker.Random.Int(10, 100)).Select(_ => new Person(faker.Name.FullName(), faker.Random.Int(1, 100))).ToArray(); - var serialized = MemoryPackSerializer.Serialize(data); - var requiredLength = Helper.GetRequiredLength(serialized); - var buffer = new RentedBufferWriter(requiredLength + 5); - try { - Helper.ReadToRenterBufferWriter(ref buffer, serialized, requiredLength); - Assert.Equal(requiredLength, buffer.Position); - } finally { - buffer?.Dispose(); - } - } -} \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/JsonContext.cs b/tests/Sharpify.Data.Tests/JsonContext.cs deleted file mode 100644 index 39974bf..0000000 --- a/tests/Sharpify.Data.Tests/JsonContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Sharpify.Data.Tests; - -[JsonSourceGenerationOptions(WriteIndented = true)] -[JsonSerializable(typeof(Color))] -public partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/Person.cs b/tests/Sharpify.Data.Tests/Person.cs deleted file mode 100644 index c28fdb7..0000000 --- a/tests/Sharpify.Data.Tests/Person.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MemoryPack; - -namespace Sharpify.Data.Tests; - -[MemoryPackable] -public readonly partial record struct Person(string Name, int Age); \ No newline at end of file diff --git a/tests/Sharpify.Data.Tests/Sharpify.Data.Tests.csproj b/tests/Sharpify.Data.Tests/Sharpify.Data.Tests.csproj deleted file mode 100644 index 987cc84..0000000 --- a/tests/Sharpify.Data.Tests/Sharpify.Data.Tests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net9.0;net8.0 - enable - latest - enable - - false - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/Sharpify.Data.Tests/User.cs b/tests/Sharpify.Data.Tests/User.cs deleted file mode 100644 index 810ef1c..0000000 --- a/tests/Sharpify.Data.Tests/User.cs +++ /dev/null @@ -1,36 +0,0 @@ - -using MemoryPack; - -namespace Sharpify.Data.Tests; - -public sealed class User : IFilterable { - public Person Person { get; init; } - - public User(Person person) { - Person = person; - } - - public static User Deserialize(ReadOnlySpan data) { - var p = MemoryPackSerializer.Deserialize(data); - return new(p); - } - - public static User[]? DeserializeMany(ReadOnlySpan data) { - var persons = MemoryPackSerializer.Deserialize(data); - return persons!.Select(p => new User(p)).ToArray(); - } - - public static byte[]? Serialize(User? value) { - if (value is null) { - return null; - } - return MemoryPackSerializer.Serialize(value.Person); - } - - public static byte[]? SerializeMany(User[]? values) { - if (values is null) { - return null; - } - return MemoryPackSerializer.Serialize(values.Select(v => v.Person).ToArray()); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/CollectionExtensionsTests.cs b/tests/Sharpify.Tests/CollectionExtensionsTests.cs index 217553e..9a22ac6 100644 --- a/tests/Sharpify.Tests/CollectionExtensionsTests.cs +++ b/tests/Sharpify.Tests/CollectionExtensionsTests.cs @@ -1,32 +1,6 @@ -using System.Buffers; - namespace Sharpify.Tests; public class CollectionExtensionsTests { - [Fact] - public void IsNullOrEmpty_GivenNullList() { - // Arrange - List? list = null; - - // Act - var result = list.IsNullOrEmpty(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsNullOrEmpty_GivenEmptyList() { - // Arrange - var list = new List(); - - // Act - var result = list.IsNullOrEmpty(); - - // Assert - Assert.True(result); - } - [Fact] public void AsSpan_GivenNonEmptyList_ReturnsCorrectSpan() { // Arrange @@ -122,40 +96,6 @@ public void GetValueRefOrAddDefault_GivenNonExistingKey_AddsNewEntryWithDefaultV #pragma warning restore } - [Fact] - public void CopyTo_CopiesDictionaryEntries() { - var dict = Enumerable.Range(1, 10).ToDictionary(i => i, i => i); - var buffer = ArrayPool>.Shared.Rent(dict.Count); - dict.CopyTo(buffer, 0); - var span = new Span>(buffer, 0, dict.Count); - Assert.Equal(dict, span.ToArray()); - buffer.ReturnBufferToSharedArrayPool(); - } - - [Fact] - public void RentBufferAndCopyEntries_ReturnRentedBuffer_Dictionary() { - var dict = Enumerable.Range(1, 10).ToDictionary(i => i, i => i); - var (buffer, entries) = dict.RentBufferAndCopyEntries(); - try { - Assert.Equal(dict, entries.ToArray()); - } finally { - buffer.ReturnBufferToSharedArrayPool(); - } - } - - [Fact] - public void Dictionary_CopyToArray() { - var dict = Enumerable.Range(1, 10).ToDictionary(i => i, i => i); - var buffer = ArrayPool>.Shared.Rent(dict.Count); - dict.CopyTo(buffer, 0); - var span = buffer.AsSpan(0, dict.Count); - try { - Assert.Equal(dict, span.ToArray()); - } finally { - buffer.ReturnBufferToSharedArrayPool(); - } - } - [Fact] public void PureSort_GivenUnsortedIntArray_ReturnsSortedIntArray() { // Arrange @@ -296,7 +236,7 @@ public void ChunkToSegments_GivenArrayWithLengthLessThanSegmentSize_ReturnsSingl // Assert Assert.Single(result); - Assert.Equal(array, result[0]); + Assert.Equal(new ArraySegment(array), result[0]); } [Fact] diff --git a/tests/Sharpify.Tests/Collections/LazyLocalPersistentDictionaryTests.cs b/tests/Sharpify.Tests/Collections/LazyLocalPersistentDictionaryTests.cs deleted file mode 100644 index 172dc1c..0000000 --- a/tests/Sharpify.Tests/Collections/LazyLocalPersistentDictionaryTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json; - -using Sharpify.Collections; - -namespace Sharpify.Tests.Collections; - -public class LazyLocalPersistentDictionaryTests { - [Fact] - public void LazyLocalPersistentDictionary_ReadKey_Null_WhenDoesNotExist() { - // Arrange - var path = Utils.Env.PathInBaseDirectory("lpdict.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new LazyLocalPersistentDictionary(path); - - // Act - var result = dict["test"]; - - // Assert - Assert.Null(result); - } - - [Fact] - public async Task LazyLocalPersistentDictionary_ReadKey_Valid_WhenExists() { - // Arrange - var path = Utils.Env.PathInBaseDirectory("lpdict.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new LazyLocalPersistentDictionary(path); - - var testJson = new { - Name = "test", - Age = 21 - }; - - // Act - await dict.UpsertAsync("one", JsonSerializer.Serialize(testJson)); - await dict.UpsertAsync("two", "2"); - - // Assert - Assert.Equal("2", dict["two"]); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/Collections/LocalPersistentDictionaryTests.cs b/tests/Sharpify.Tests/Collections/LocalPersistentDictionaryTests.cs deleted file mode 100644 index bd8e327..0000000 --- a/tests/Sharpify.Tests/Collections/LocalPersistentDictionaryTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Sharpify.Collections; - -namespace Sharpify.Tests.Collections; - -public class LocalPersistentDictionaryTests { - private readonly ITestOutputHelper _testOutputHelper; - - public LocalPersistentDictionaryTests(ITestOutputHelper testOutputHelper) { - _testOutputHelper = testOutputHelper; - } - - [Fact] - public void LocalPersistentDictionary_ReadKey_Null_WhenDoesNotExist() { - // Arrange - var path = Utils.Env.PathInBaseDirectory("pdict.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - var result = dict["test"]; - - // Assert - Assert.Null(result); - } - - [Fact] - public async Task LocalPersistentDictionary_ReadKey_Valid_WhenExists() { - // Arrange - var path = Utils.Env.PathInBaseDirectory("pdict.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - await dict.UpsertAsync("one", "1"); - - // Assert - Assert.Equal("1", dict["one"]); - } - - [Fact] - public async Task LocalPersistentDictionary_GetOrCreate() { - // Arrange - var path = Utils.Env.PathInBaseDirectory("pdict.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - var result = await dict.GetOrCreateAsync("one", "1"); - var check = dict["one"] is "1"; - - // Assert - Assert.Equal("1", result); - Assert.True(check); - } - - [Fact] - public async Task LocalPersistentDictionary_Upsert_Concurrent() { - // Arrange - var filename = Random.Shared.Next(999, 10000).ToString(); - var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - Task[] upsertTasks = [ - Task.Run(async () => await dict.UpsertAsync("one", "1"), TestContext.Current.CancellationToken), - Task.Run(async () => await dict.UpsertAsync("two", "2"), TestContext.Current.CancellationToken), - Task.Run(async () => await dict.UpsertAsync("three", "3"), TestContext.Current.CancellationToken), - Task.Run(async () => await dict.UpsertAsync("four", "4"), TestContext.Current.CancellationToken), - Task.Run(async () => await dict.UpsertAsync("five", "5"), TestContext.Current.CancellationToken), - ]; - await Task.WhenAll(upsertTasks); - - // Assert - // dict.SerializedCount.Should().BeLessThanOrEqualTo(upsertTasks.Length); - _testOutputHelper.WriteLine($"PersistentDictionary serialized count: {dict.SerializedCount}"); - // This is checking that the dictionary was serialized less than the number of upserts. - // Ideally with perfectly concurrent updates, the dictionary would only be serialized once. - // The reason not to check for 1 is that the tasks may not be executed perfectly in parallel. - var sdict = new LocalPersistentDictionary(path); - Assert.Equal(upsertTasks.Length, sdict.Count); - File.Delete(path); - } - - [Fact] - public async Task LocalPersistentDictionary_Upsert_Sequential_NoItemsMissing() { - // Arrange - var filename = Random.Shared.Next(999, 10000).ToString(); - var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - await dict.UpsertAsync("one", "1"); - await dict.UpsertAsync("two", "2"); - await dict.UpsertAsync("three", "3"); - await dict.UpsertAsync("four", "4"); - await dict.UpsertAsync("five", "5"); - - // Assert - var sdict = new LocalPersistentDictionary(path); - Assert.Equal(5, sdict.Count); - File.Delete(path); - } - - [Fact] - public async Task LocalPersistentDictionary_GenericGetAndUpsert() { - // Arrange - var filename = Random.Shared.Next(999, 10000).ToString(); - var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); - if (File.Exists(path)) { - File.Delete(path); - } - var dict = new TestLocalPersistentDictionary(path); - - // Act - await dict.UpsertAsync("one", 1); - await dict.UpsertAsync("two", 2); - var sdict = new LocalPersistentDictionary(path); - int one = await sdict.GetOrCreateAsync("one", 0); - int two = await sdict.GetOrCreateAsync("two", 0); - - // Assert - Assert.Equal(1, one); - Assert.Equal(2, two); - File.Delete(path); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/Collections/RentedBufferWriterTests.cs b/tests/Sharpify.Tests/Collections/RentedBufferWriterTests.cs deleted file mode 100644 index 24624d6..0000000 --- a/tests/Sharpify.Tests/Collections/RentedBufferWriterTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Sharpify.Collections; - -namespace Sharpify.Tests.Collections; - -public class RentedBufferWriterTests { - [Fact] - public void RentedBufferWriter_InvalidCapacity_Throws() { - // Arrange - Action act = () => { - using var buffer = new RentedBufferWriter(-1); - }; - - // Act & Assert - Assert.Throws(act); - } - - [Fact] - public void RentedBufferWriter_Capacity0IsDisabled() { - // Arrange - using var buffer = new RentedBufferWriter(0); - - // Assert - Assert.True(buffer.IsDisabled); - } - - [Fact] - public void RentedBufferWriter_WriteToSpan() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var span = buffer.GetSpan(); - "Hello".AsSpan().CopyTo(span); - buffer.Advance(5); - - // Assert - Assert.Equal("Hello", buffer.WrittenSpan); - } - - [Fact] - public void RentedBufferWriter_WriteAndAdvance() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - buffer.WriteAndAdvance("Hello"); - - // Assert - Assert.Equal("Hello", buffer.WrittenSpan); - } - - [Fact] - public void RentedBufferWriter_UseRefToWriteValue() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - ref var arr = ref buffer.GetReferenceUnsafe(); - var length = WriteOnes(ref arr, 5); - buffer.Advance(length); - - // Assert - Assert.Equal([1, 1, 1, 1, 1], buffer.WrittenSpan); - - static int WriteOnes(ref int[] buffer, int length) { - for (var i = 0; i < length; i++) { - buffer[i] = 1; - } - - return length; - } - } - - [Fact] - public void RentedBufferWriter_GetSpanSlice() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var span = buffer.GetSpan(); - "Hello".AsSpan().CopyTo(span); - buffer.Advance(5); - - // Assert - Assert.Equal("Hel", buffer.GetSpanSlice(0, 3)); - } - - [Fact] - public void RentedBufferWriter_WriteToMemory() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var mem = buffer.GetMemory(); - "Hello".AsSpan().CopyTo(mem.Span); - buffer.Advance(5); - - // Assert - Assert.Equal("Hello".ToCharArray(), buffer.WrittenSegment); - } - - [Fact] - public void RentedBufferWriter_GetMemorySlice() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var mem = buffer.GetMemory(); - "Hello".AsSpan().CopyTo(mem.Span); - buffer.Advance(5); - - // Assert - Assert.Equal("Hello", buffer.GetMemorySlice(0, 5).Span); - } - - [Fact] - public void RentedBufferWriter_WrittenSegment() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var span = buffer.GetSpan(); - "Hello".AsSpan().CopyTo(span); - buffer.Advance(5); - - // Assert - Assert.Equal("Hello".ToCharArray(), buffer.WrittenSegment); - } - - [Fact] - public void RentedBufferWriter_Reset() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var span = buffer.GetSpan(); - "Hello".AsSpan().CopyTo(span); - buffer.Advance(5); - buffer.Reset(); - - // Assert - Assert.Equal(ReadOnlySpan.Empty, buffer.WrittenSpan); - } - - [Fact] - public void RentedBufferWriter_ActualCapacity() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Assert - Assert.True(buffer.ActualCapacity >= 20); - } - - [Fact] - public void RentedBufferWriter_FreeCapacity() { - // Arrange - using var buffer = new RentedBufferWriter(20); - - // Act - var span = buffer.GetSpan(); - "Hello".AsSpan().CopyTo(span); - buffer.Advance(5); - - // Assert - Assert.True(buffer.FreeCapacity >= 15); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/Collections/TestLocalPersistentDictionary.cs b/tests/Sharpify.Tests/Collections/TestLocalPersistentDictionary.cs deleted file mode 100644 index 71d5f39..0000000 --- a/tests/Sharpify.Tests/Collections/TestLocalPersistentDictionary.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Sharpify.Collections; - -namespace Sharpify.Tests.Collections; - -public class TestLocalPersistentDictionary : LocalPersistentDictionary { - private volatile int _serializedCount; - - public TestLocalPersistentDictionary(string path) : base(path) { - } - - public int SerializedCount => _serializedCount; - - public override async Task SerializeDictionaryAsync() { - Interlocked.Increment(ref _serializedCount); - await base.SerializeDictionaryAsync(); - } -} diff --git a/tests/Sharpify.Tests/SerializableObjectTests.cs b/tests/Sharpify.Tests/SerializableObjectTests.cs deleted file mode 100644 index f79845d..0000000 --- a/tests/Sharpify.Tests/SerializableObjectTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Sharpify.Tests; - -[Collection("SerializableObjectTests")] -public class SerializableObjectTests { - [Fact] - public async Task Constructor_Throws_When_Filename_Is_Invalid() { - var file = await TempFile.CreateAsync(); - var dir = Path.GetDirectoryName(file)!; - // Arrange - var action = () => new MonitoredSerializableObject(dir, JsonContext.Default.Configuration); // no filename - - // Act & Assert - Assert.Throws(action); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task Constructor_Creates_File_When_File_Does_Not_Exist() { - // Arrange - var file = await TempFile.CreateAsync(); - var action = () => new MonitoredSerializableObject(file.Path, JsonContext.Default.Configuration); - - // Act - action(); - - // Assert - Assert.True(File.Exists(file)); - await file.DeleteAsync(); - } - - [Fact] - public async Task Constructor_Deserializes_File_When_File_Exists() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - var action = () => new MonitoredSerializableObject(file.Path, config, JsonContext.Default.Configuration); - - // Act - action(); - // Assert - Assert.True(File.Exists(file)); - // Act - using var obj = new MonitoredSerializableObject(file.Path, JsonContext.Default.Configuration); - // Assert - Assert.Equal(config, obj.Value); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task Modify_SerializesProperly() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - using var obj = new MonitoredSerializableObject(file.Path, config, JsonContext.Default.Configuration); - const string newName = "Jane Doe"; - - // Act - obj.Modify(c => c with { Name = newName }); - // Assert - Assert.Equal(newName, obj.Value.Name); - using var obj2 = new MonitoredSerializableObject(file.Path, JsonContext.Default.Configuration); - Assert.Equal(newName, obj2.Value.Name); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task Modify_FiresEventOnce_WithProperArgs() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - using var obj = new MonitoredSerializableObject(file.Path, config, JsonContext.Default.Configuration); - const string newName = "Jane Doe"; - int count = 0; - Configuration lastValue = default; - obj.OnChanged += (sender, e) => { - count++; - lastValue = e.Value; - }; - - // Act - obj.Modify(c => c with { Name = newName }); - // Assert - Assert.Equal(1, count); - Assert.Equal(newName, lastValue.Name); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task OnFileChanged_DoesntChangeWhenFileIsEmpty() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - using var obj = new MonitoredSerializableObject(file.Path, config, JsonContext.Default.Configuration); - - // Act - await File.WriteAllTextAsync(file, "", TestContext.Current.CancellationToken); - - // Assert - Assert.Equal("John Doe", obj.Value.Name); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task OnFileChanged_DoesntChangeWhenFileIsInvalid() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - using var obj = new MonitoredSerializableObject(file, config, JsonContext.Default.Configuration); - int count = 0; - obj.OnChanged += (sender, e) => { - Interlocked.Increment(ref count); - }; - - // Act - await File.WriteAllTextAsync(file, "invalid json", TestContext.Current.CancellationToken); - // Assert - Assert.Equal(0, count); - - // Cleanup - await file.DeleteAsync(); - } - - [Fact] - public async Task OnFileChanged_ChangesWhenFileIsValid() { - // Arrange - var file = await TempFile.CreateAsync(); - var config = new Configuration { Name = "John Doe", Age = 42 }; - using var obj = new MonitoredSerializableObject(file, config, JsonContext.Default.Configuration); - // Assert - obj.OnChanged += (sender, e) => Assert.Equal("Jane", e.Value.Name); - - // Act - await File.WriteAllTextAsync(file, JsonSerializer.Serialize(config with { Name = "Jane" }, JsonContext.Default.Configuration), TestContext.Current.CancellationToken); - - // Cleanup - await file.DeleteAsync(); - } -} -internal record struct Configuration { - public string Name { get; set; } - public int Age { get; set; } -} - -[JsonSourceGenerationOptions(WriteIndented = true)] -[JsonSerializable(typeof(Configuration))] -internal partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/tests/Sharpify.Tests/Sharpify.Tests.csproj b/tests/Sharpify.Tests/Sharpify.Tests.csproj index e4e0fb8..9d18b27 100644 --- a/tests/Sharpify.Tests/Sharpify.Tests.csproj +++ b/tests/Sharpify.Tests/Sharpify.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 true true enable @@ -10,14 +10,17 @@ - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/Sharpify.Tests/StringExtensionsTests.cs b/tests/Sharpify.Tests/StringExtensionsTests.cs index 545df71..36e494e 100644 --- a/tests/Sharpify.Tests/StringExtensionsTests.cs +++ b/tests/Sharpify.Tests/StringExtensionsTests.cs @@ -2,109 +2,6 @@ namespace Sharpify.Tests; #pragma warning disable public class StringExtensionsTests { - [Fact] - public void IsNullOrEmpty_GivenNullString_ReturnsTrue() { - // Arrange - string value = null; - - // Act - var result = value.IsNullOrEmpty(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsNullOrEmpty_GivenEmptyString_ReturnsTrue() { - // Arrange - const string value = ""; - - // Act - var result = value.IsNullOrEmpty(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsNullOrWhiteSpace_GivenNullString_ReturnsTrue() { - // Arrange - string value = null; - - // Act - var result = value.IsNullOrWhiteSpace(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsNullOrWhiteSpace_GivenEmptyString_ReturnsTrue() { - // Arrange - const string value = ""; - - // Act - var result = value.IsNullOrWhiteSpace(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsNullOrWhiteSpace_GivenWhiteSpaceString_ReturnsTrue() { - // Arrange - const string value = " "; - - // Act - var result = value.IsNullOrWhiteSpace(); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("0", 0)] - [InlineData("1", 1)] - [InlineData("123", 123)] - [InlineData("2147483647", 2147483647)] // int.MaxValue - public void TryConvertToInt32_ValidString_ReturnsTrue(string input, int expected) { - bool result = input.AsSpan().TryConvertToInt32(out var output); - - Assert.True(result); - Assert.Equal(expected, output); - } - - [Theory] - [InlineData("")] - [InlineData("-1 5")] // whitespace - [InlineData("214748364841232131231")] // larger than int.MaxValue - [InlineData("1.23")] // decimal - [InlineData("123abc")] // alphanumeric - public void TryConvertToInt32_InvalidString_ReturnsFalse(string input) { - bool result = input.AsSpan().TryConvertToInt32(out var output); - - Assert.False(result); - Assert.Equal(0, output); // Ensure that the value is not changed in case of failure - } - - // Tests for Concat - [Theory] - [InlineData("", "", "")] - [InlineData("hello", "", "hello")] - [InlineData("", "world", "world")] - [InlineData("hello", "world", "helloworld")] - public void Concat_WithVariousInputs_ReturnsCorrectResult( - string value, string suffixString, string expectedResult) { - // Arrange - ReadOnlySpan suffix = suffixString; - - // Act - string result = value.Concat(suffix); - - // Assert - Assert.Equal(expectedResult, result); - } - // Tests for ToTitle [Theory] [InlineData("", "")] diff --git a/tests/Sharpify.Tests/TempFile.cs b/tests/Sharpify.Tests/TempFile.cs index fb59e31..3d087b1 100644 --- a/tests/Sharpify.Tests/TempFile.cs +++ b/tests/Sharpify.Tests/TempFile.cs @@ -1,7 +1,7 @@ namespace Sharpify.Tests; public record TempFile { - public string Path { get; } + public string FilePath { get; } private const int Retries = 5; public static async Task CreateAsync() { @@ -20,14 +20,14 @@ public static async Task CreateAsync() { } private TempFile() { - Path = Utils.Env.PathInBaseDirectory(Random.Shared.Next(1000000, 9999999).ToString()); - using var _ = File.Create(Path); + FilePath = Path.Combine(AppContext.BaseDirectory, Random.Shared.Next(1000000, 9999999).ToString()); + using var _ = File.Create(FilePath); } - public static implicit operator string(TempFile file) => file.Path; + public static implicit operator string(TempFile file) => file.FilePath; public async Task DeleteAsync() { - if (!File.Exists(Path)) { + if (!File.Exists(FilePath)) { return; } @@ -37,7 +37,7 @@ public async Task DeleteAsync() { bool wasDeleted = false; do { try { - File.Delete(Path); + File.Delete(FilePath); wasDeleted = true; } catch { if (Interlocked.Decrement(ref retries) >= 0) { diff --git a/tests/Sharpify.Tests/ThreadSafeTests.cs b/tests/Sharpify.Tests/ThreadSafeTests.cs deleted file mode 100644 index 469f754..0000000 --- a/tests/Sharpify.Tests/ThreadSafeTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace Sharpify.Tests; - -public class ThreadSafeTests { - [Fact] - public void ThreadSafe_EmptyConstructor() { - ThreadSafe wrapper = new(); - - Assert.Equal(0, wrapper.Value); - } - - [Fact] - public void ThreadSafe_ValueConstructor() { - ThreadSafe wrapper = new(42); - - int result = wrapper.Value; - - Assert.Equal(42, result); - } - - [Fact] - public void ThreadSafe_UpdateValue() { - ThreadSafe wrapper = new(5); - const int newValue = 99; - - int result = wrapper.Modify(_ => newValue); - - Assert.Equal(newValue, result); - } - - [Theory] - [InlineData(1, 2, 3)] - [InlineData(2, 3, 5)] - [InlineData(3, 4, 7)] - public void ThreadSafe_ModifyValue(int original, int addition, int expected) { - ThreadSafe wrapper = new(original); - - int result = wrapper.Modify(value => value + addition); - - Assert.Equal(expected, result); - } - - [Theory] - [InlineData(100,100)] - [InlineData(200, 200)] - [InlineData(300, 300)] - public async Task ThreadSafe_MultiThreadedAccess(int amount, int expected) { - ThreadSafe wrapper = new(0); - - var tasks = Enumerable.Range(0, amount).AsParallel().Select(i => Task.Run(() => wrapper.Modify(value => value + 1))); - await Task.WhenAll(tasks); - - Assert.Equal(expected, wrapper.Value); - } - - [Fact] - public void ThreadSafe_GetHashCode() { - int val = 42; - - ThreadSafe wrapper = new(val); - - int actual = wrapper.GetHashCode(); - int expected = val.GetHashCode(); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData(1, 1)] - [InlineData(2, 2)] - [InlineData(3, 3)] - [InlineData(-4, -4)] - public void ThreadSafe_Equals(int actual, int expected) { - ThreadSafe wrapper = new(actual); - - Assert.True(wrapper.Equals(expected)); - } - - [Fact] - public void ThreadSafe_Equals_Null() { - int val = 42; - - ThreadSafe wrapper = new(val); - - Assert.False(wrapper.Equals(null)); - } - - [Fact] - public void ThreadSafe_Equals_ThreadSafe() { - int val = 42; - - ThreadSafe wrapper = new(val); - - Assert.True(wrapper.Equals(new ThreadSafe(val))); - } - - [Fact] - public void ThreadSafe_Equals_Object() { - int val = 42; - - ThreadSafe wrapper = new(val); - var other = (object)new ThreadSafe(val); - - Assert.True(wrapper.Equals(other)); - } - - [Fact] - public void ThreadSafe_Equals_NullObject() { - int val = 42; - - ThreadSafe wrapper = new(val); - object? other = null; - - Assert.False(wrapper.Equals(other)); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/UnmanagedExtensionsTests.cs b/tests/Sharpify.Tests/UnmanagedExtensionsTests.cs deleted file mode 100644 index 737615f..0000000 --- a/tests/Sharpify.Tests/UnmanagedExtensionsTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Sharpify.Tests; - -public class UnmanagedExtensionsTests { - public enum ExampleEnum { - FirstValue, - SecondValue, - ThirdValue - } - - [Theory] - [InlineData("FirstValue", true, ExampleEnum.FirstValue)] - [InlineData("SecondValue", true, ExampleEnum.SecondValue)] - [InlineData("ThirdValue", true, ExampleEnum.ThirdValue)] - [InlineData("InvalidValue", false, default(ExampleEnum))] - [InlineData("", false, default(ExampleEnum))] - public void TryParseAsEnum_WithVariousInputs_ReturnsCorrectResult( - string value, bool expectedResult, ExampleEnum expectedEnum) { - // Act - bool result = value.TryParseAsEnum(out ExampleEnum parsedEnum); - - // Assert - Assert.Equal(expectedResult, result); - Assert.Equal(expectedEnum, parsedEnum); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/UtilsDateAndTimeTests.cs b/tests/Sharpify.Tests/UtilsDateAndTimeTests.cs index 3edfc01..ceffb3e 100644 --- a/tests/Sharpify.Tests/UtilsDateAndTimeTests.cs +++ b/tests/Sharpify.Tests/UtilsDateAndTimeTests.cs @@ -3,47 +3,6 @@ namespace Sharpify.Tests; public partial class UtilsTests { - [Theory] - [InlineData(0.00001, "0ms")] - [InlineData(0.01, "10ms")] - [InlineData(0.5, "500ms")] - [InlineData(1, "01:000s")] - [InlineData(59.99, "59:990s")] - [InlineData(60, "01:00m")] - [InlineData(61, "01:01m")] - [InlineData(121.234, "02:01m")] - public void FormatTimeSpan_ReturnsFormattedSpan(double seconds, string expected) { - // Arrange - var elapsed = TimeSpan.FromSeconds(seconds); - using var owner = MemoryPool.Shared.Rent(30); - - // Act - ReadOnlySpan result = Utils.DateAndTime.FormatTimeSpan(elapsed, owner.Memory.Span); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [InlineData(0.00001, "0ms")] - [InlineData(0.01, "10ms")] - [InlineData(0.5, "500ms")] - [InlineData(1, "01:000s")] - [InlineData(59.99, "59:990s")] - [InlineData(60, "01:00m")] - [InlineData(61, "01:01m")] - [InlineData(121.234, "02:01m")] - public void FormatTimeSpan_ReturnsFormattedString(double seconds, string expected) { - // Arrange - var elapsed = TimeSpan.FromSeconds(seconds); - - // Act - string result = Utils.DateAndTime.FormatTimeSpan(elapsed); - - // Assert - Assert.Equal(expected, result); - } - [Fact] public void ToTimeStamp_ReturnsFormattedSpan() { // Arrange @@ -51,10 +10,10 @@ public void ToTimeStamp_ReturnsFormattedSpan() { using var owner = MemoryPool.Shared.Rent(30); // Act - ReadOnlySpan result = Utils.DateAndTime.FormatTimeStamp(dateTime, owner.Memory.Span); + ReadOnlySpan result = Utils.FormatTimeStamp(dateTime, owner.Memory.Span); // Assert - Assert.Equal("1355-6-Apr-22", result); + Assert.Equal("1355-06-Apr-22", result); } [Fact] @@ -63,34 +22,9 @@ public void ToTimeStamp_ReturnsFormattedString() { var dateTime = new DateTime(2022, 04, 06, 13, 55, 00); // Act - string result = Utils.DateAndTime.FormatTimeStamp(dateTime); - - // Assert - Assert.Equal("1355-6-Apr-22", result); - } - - [Fact] - public async Task GetCurrentTimeAsync_ReturnsCurrentTime() { - // Arrange - var expected = DateTime.Now; - - // Act - var result = await Utils.DateAndTime.GetCurrentTimeAsync(); - - // Assert - Assert.Equal(expected, result, TimeSpan.FromSeconds(1)); - } - - [Fact] - public async Task GetCurrentTimeInBinaryAsync_ReturnsCurrentTimeInBinary() { - // Arrange - var expected = DateTime.Now; - - // Act - var result = await Utils.DateAndTime.GetCurrentTimeInBinaryAsync(); - var fromResult = DateTime.FromBinary(result); + string result = Utils.FormatTimeStamp(dateTime); // Assert - Assert.Equal(expected, fromResult, TimeSpan.FromSeconds(1)); + Assert.Equal("1355-06-Apr-22", result); } } \ No newline at end of file diff --git a/tests/Sharpify.Tests/UtilsMathematicsTests.cs b/tests/Sharpify.Tests/UtilsMathematicsTests.cs index 5fce068..dfe071d 100644 --- a/tests/Sharpify.Tests/UtilsMathematicsTests.cs +++ b/tests/Sharpify.Tests/UtilsMathematicsTests.cs @@ -11,7 +11,7 @@ public void RollingAverage_WithVariousInputs_ReturnsCorrectResult( expectedResult = Math.Round(expectedResult, 15); // Act - double result = Utils.Mathematics.RollingAverage(val, newVal, count); + double result = Utils.RollingAverage(val, newVal, count); result = Math.Round(result, 3); // Assert @@ -24,7 +24,7 @@ public void RollingAverage_WithVariousInputs_ReturnsCorrectResult( [InlineData(11, 39916800)] public void Factorial_ValidInput_ValidResult(double n, double expected) { // Act - var result = Utils.Mathematics.Factorial(n); + var result = Utils.Factorial(n); // Assert Assert.Equal(expected, result); @@ -37,7 +37,7 @@ public void Factorial_ValidInput_ValidResult(double n, double expected) { [InlineData(33, 3524578)] public void FibonacciApproximation_ValidInput_ValidResult(int n, double expected) { // Act - var result = Utils.Mathematics.FibonacciApproximation(n); + var result = Utils.FibonacciApproximation(n); // Assert const double margin = 0.01; diff --git a/tests/Sharpify.Tests/UtilsStringsTests.cs b/tests/Sharpify.Tests/UtilsStringsTests.cs deleted file mode 100644 index b5052f8..0000000 --- a/tests/Sharpify.Tests/UtilsStringsTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Sharpify.Tests; - -public partial class UtilsTests { - [Theory] - [InlineData(0.0, "0 B")] - [InlineData(1023.0, "1,023 B")] - [InlineData(1024.0, "1 KB")] - [InlineData(1057.393, "1.03 KB")] - [InlineData(1048576.0, "1 MB")] - [InlineData(1073741824.0, "1 GB")] - [InlineData(1099511627776.0, "1 TB")] - [InlineData(1125899906842624.0, "1 PB")] - public void FormatBytes_DoubleWithVariousInputs_ReturnsCorrectResult( - double bytes, string expectedResult) { - // Act - string result = Utils.Strings.FormatBytes(bytes); - - // Assert - Assert.Equal(expectedResult, result); - } - - [Theory] - [InlineData(0L, "0 B")] - [InlineData(1023L, "1,023 B")] - [InlineData(1024L, "1 KB")] - [InlineData(1048576L, "1 MB")] - [InlineData(1073741824L, "1 GB")] - [InlineData(1099511627776L, "1 TB")] - [InlineData(1125899906842624L, "1 PB")] - public void FormatBytes_LongWithVariousInputs_ReturnsCorrectResult( - long bytes, string expectedResult) { - // Act - string result = Utils.Strings.FormatBytes(bytes); - - // Assert - Assert.Equal(expectedResult, result); - } - - [Theory] - [InlineData(1610612736L, "1.5 GB")] // 1.5 * 1024^3 - [InlineData(1627389952L, "1.52 GB")] // 1.52 * 1024^3 - [InlineData(1644167168L, "1.53 GB")] // 1.53 * 1024^3 - public void FormatBytes_LongWithNonRoundedInputs_ReturnsCorrectResult( - long bytes, string expectedResult) { - // Act - string result = Utils.Strings.FormatBytes(bytes); - - // Assert - Assert.Equal(expectedResult, result); - } - - [Theory] - [InlineData(1610612736.0, "1.5 GB")] // 1.5 * 1024^3 - [InlineData(1627389952.0, "1.52 GB")] // 1.52 * 1024^3 - [InlineData(1644167168.0, "1.53 GB")] // 1.53 * 1024^3 - public void FormatBytes_DoubleWithNonRoundedInputs_ReturnsCorrectResult( - double bytes, string expectedResult) { - // Act - string result = Utils.Strings.FormatBytes(bytes); - - // Assert - Assert.Equal(expectedResult, result); - } - - [Fact] - public void FormatBytes_Double_HasEnoughCapacity() { - // Arrange - Action act = () => _ = Utils.Strings.FormatBytes(double.MaxValue); - - // Assert - act(); - } - - [Fact] - public void FormatBytes_Long_HasEnoughCapacity() { - // Arrange - Action act = () => _ = Utils.Strings.FormatBytes(long.MaxValue); - - // Assert - act(); - } -} \ No newline at end of file diff --git a/tests/Sharpify.Tests/UtilsUnsafeTests.cs b/tests/Sharpify.Tests/UtilsUnsafeTests.cs index d0218bb..caf0fa3 100644 --- a/tests/Sharpify.Tests/UtilsUnsafeTests.cs +++ b/tests/Sharpify.Tests/UtilsUnsafeTests.cs @@ -4,7 +4,7 @@ public partial class UtilsTests { [Fact] public void CreateIntegerPredicate_ForCharIsDigit_Valid() { // Arrange - var predicate = Utils.Unsafe.CreateIntegerPredicate(char.IsDigit); + var predicate = Utils.CreateIntegerPredicate(char.IsDigit); // Act var one = predicate('1'); @@ -21,7 +21,7 @@ public void TryUnbox_ForValidInput_ValidResult() { var obj = (object) 5; // Act - var result = Utils.Unsafe.TryUnbox(obj, out var value); + var result = Utils.TryUnbox(obj, out var value); // Assert Assert.True(result); @@ -35,7 +35,7 @@ public void AsMutableSpan_ForValidInput_ValidResult() { var span = str.AsSpan(); // Act - var mutableSpan = Utils.Unsafe.AsMutableSpan(span); + var mutableSpan = Utils.AsMutableSpan(span); mutableSpan[2] = '1'; // Assert