diff --git a/Assets/cortex-all.svg b/Assets/cortex-all.svg new file mode 100644 index 0000000..8332ac9 --- /dev/null +++ b/Assets/cortex-all.svg @@ -0,0 +1 @@ +Cortex \ No newline at end of file diff --git a/Cortex.sln b/Cortex.sln index 5449f85..025b0b9 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -53,6 +53,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.PostgreSQL", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.MongoDb", "src\Cortex.Streams.MongoDb\Cortex.Streams.MongoDb.csproj", "{FC86D3AB-778D-45D7-AF36-1F89FC16DE55}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.Elasticsearch", "src\Cortex.Streams.Elasticsearch\Cortex.Streams.Elasticsearch.csproj", "{4D1F117D-48D7-47AD-9DAC-3B2DB45E628A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Types", "src\Cortex.Types\Cortex.Types.csproj", "{64E12D4C-FBB2-4004-8316-C886CBFC614B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,6 +162,14 @@ Global {FC86D3AB-778D-45D7-AF36-1F89FC16DE55}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC86D3AB-778D-45D7-AF36-1F89FC16DE55}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC86D3AB-778D-45D7-AF36-1F89FC16DE55}.Release|Any CPU.Build.0 = Release|Any CPU + {4D1F117D-48D7-47AD-9DAC-3B2DB45E628A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D1F117D-48D7-47AD-9DAC-3B2DB45E628A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D1F117D-48D7-47AD-9DAC-3B2DB45E628A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D1F117D-48D7-47AD-9DAC-3B2DB45E628A}.Release|Any CPU.Build.0 = Release|Any CPU + {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 66e606b..e3c027d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Cortex](./Assets/cortex-all.svg) + # Cortex Data Framework **Cortex Data Framework** is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. @@ -6,7 +8,7 @@ [![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Streams?label=Cortex.Streams)](https://www.nuget.org/packages/Cortex.Streams) [![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) -[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.com/invite/4Gfe6mhJ) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) ## Key Features - **Modular Architecture**: Comprises distinct, interchangeable modules for streaming, state management, and connectors, allowing developers to choose components that best fit their requirements. @@ -72,6 +74,9 @@ - **Cortex.Streams.Http:** Implementation of Http Source and Sink operators. [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Streams.Http?label=Cortex.Streams.Http)](https://www.nuget.org/packages/Cortex.Streams.Http) +- **Cortex.Streams.Elasticsearch:** Implementation of Elasticsearch Sink operators. +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Streams.Elasticsearch?label=Cortex.Streams.Elasticsearch)](https://www.nuget.org/packages/Cortex.Streams.Elasticsearch) + - **Cortex.Streams.CDC.MSSqlServer:** CDC adapter for Microsoft Sql Server. [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Streams.CDC.MSSqlServer?label=Cortex.Streams.CDC.MSSqlServer)](https://www.nuget.org/packages/Cortex.Streams.CDC.MSSqlServer) @@ -111,6 +116,9 @@ - **Cortex.Telemetry.OpenTelemetry:** Adds support for Open Telemetry. [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Telemetry.OpenTelemetry?label=Cortex.Telemetry.OpenTelemetry)](https://www.nuget.org/packages/Cortex.Telemetry.OpenTelemetry) +- **Cortex.Types:** Use complex types like OneOf, AllOf and AnyOf +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Types?label=Cortex.Types)](https://www.nuget.org/packages/Cortex.Types) + ## Getting Started @@ -324,7 +332,7 @@ We'd love to hear from you! Whether you have questions, feedback, or need suppor - Email: cortex@buildersoft.io - Website: https://buildersoft.io - GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) -- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.com/invite/4Gfe6mhJ) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. diff --git a/src/Cortex.Mediator/Assets/andyX.png b/src/Cortex.Mediator/Assets/andyX.png index 5de0383..101a1fb 100644 Binary files a/src/Cortex.Mediator/Assets/andyX.png and b/src/Cortex.Mediator/Assets/andyX.png differ diff --git a/src/Cortex.States.Cassandra/Assets/cortex.png b/src/Cortex.States.Cassandra/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.Cassandra/Assets/cortex.png and b/src/Cortex.States.Cassandra/Assets/cortex.png differ diff --git a/src/Cortex.States.ClickHouse/Assets/cortex.png b/src/Cortex.States.ClickHouse/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.ClickHouse/Assets/cortex.png and b/src/Cortex.States.ClickHouse/Assets/cortex.png differ diff --git a/src/Cortex.States.MSSqlServer/Assets/cortex.png b/src/Cortex.States.MSSqlServer/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.MSSqlServer/Assets/cortex.png and b/src/Cortex.States.MSSqlServer/Assets/cortex.png differ diff --git a/src/Cortex.States.MongoDb/Assets/cortex.png b/src/Cortex.States.MongoDb/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.MongoDb/Assets/cortex.png and b/src/Cortex.States.MongoDb/Assets/cortex.png differ diff --git a/src/Cortex.States.PostgreSQL/Assets/cortex.png b/src/Cortex.States.PostgreSQL/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.PostgreSQL/Assets/cortex.png and b/src/Cortex.States.PostgreSQL/Assets/cortex.png differ diff --git a/src/Cortex.States.RocksDb/Assets/cortex.png b/src/Cortex.States.RocksDb/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.RocksDb/Assets/cortex.png and b/src/Cortex.States.RocksDb/Assets/cortex.png differ diff --git a/src/Cortex.States.SQLite/Assets/cortex.png b/src/Cortex.States.SQLite/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States.SQLite/Assets/cortex.png and b/src/Cortex.States.SQLite/Assets/cortex.png differ diff --git a/src/Cortex.States/Assets/cortex.png b/src/Cortex.States/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.States/Assets/cortex.png and b/src/Cortex.States/Assets/cortex.png differ diff --git a/src/Cortex.Streams.AWSSQS/Assets/cortex.png b/src/Cortex.Streams.AWSSQS/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.AWSSQS/Assets/cortex.png and b/src/Cortex.Streams.AWSSQS/Assets/cortex.png differ diff --git a/src/Cortex.Streams.AzureBlobStorage/Assets/cortex.png b/src/Cortex.Streams.AzureBlobStorage/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.AzureBlobStorage/Assets/cortex.png and b/src/Cortex.Streams.AzureBlobStorage/Assets/cortex.png differ diff --git a/src/Cortex.Streams.AzureServiceBus/Assets/cortex.png b/src/Cortex.Streams.AzureServiceBus/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.AzureServiceBus/Assets/cortex.png and b/src/Cortex.Streams.AzureServiceBus/Assets/cortex.png differ diff --git a/src/Cortex.Streams.Elasticsearch/Assets/cortex.png b/src/Cortex.Streams.Elasticsearch/Assets/cortex.png new file mode 100644 index 0000000..101a1fb Binary files /dev/null and b/src/Cortex.Streams.Elasticsearch/Assets/cortex.png differ diff --git a/src/Cortex.Streams.Elasticsearch/Assets/license.md b/src/Cortex.Streams.Elasticsearch/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Streams.Elasticsearch/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Streams.Elasticsearch/Cortex.Streams.Elasticsearch.csproj b/src/Cortex.Streams.Elasticsearch/Cortex.Streams.Elasticsearch.csproj new file mode 100644 index 0000000..7c4a12d --- /dev/null +++ b/src/Cortex.Streams.Elasticsearch/Cortex.Streams.Elasticsearch.csproj @@ -0,0 +1,56 @@ + + + + net9.0;net8.0 + + 1.0.1 + 1.0.1 + Buildersoft Cortex Framework + Buildersoft + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + Cortex Data Framework is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. + + + https://github.com/buildersoftio/cortex + cortex cdc eda streaming distributed streams states elasticsearch + + 1.0.1 + license.md + cortex.png + Cortex.Streams.Elasticsearch + True + True + True + + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + README.md + + + + + True + \ + + + True + + + + True + + + + + + + + + + + + + + diff --git a/src/Cortex.Streams.Elasticsearch/ElasticsearchSinkOperator.cs b/src/Cortex.Streams.Elasticsearch/ElasticsearchSinkOperator.cs new file mode 100644 index 0000000..811d3e4 --- /dev/null +++ b/src/Cortex.Streams.Elasticsearch/ElasticsearchSinkOperator.cs @@ -0,0 +1,340 @@ +using Cortex.States; +using Cortex.States.Operators; +using Cortex.Streams.Operators; +using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Core.Bulk; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Cortex.Streams.Elasticsearch +{ + /// + /// A sink operator that writes data to an Elasticsearch index in bulk. + /// + /// If some documents fail to index, they are stored in an IDataStore for later retries. + /// A background service attempts to resend failed documents at a specified interval. + /// + /// Type of the document to be indexed. + public class ElasticsearchSinkOperator : ISinkOperator, IStatefulOperator + { + private readonly ElasticsearchClient _client; + private readonly string _indexName; + private readonly IDataStore _failedDocumentsStore; + private readonly ILogger> _logger; + + // Local in-memory buffer for the current batch to be flushed. + private readonly List _currentBatch; + private readonly object _batchLock = new object(); + + // The maximum number of documents to collect before we issue a bulk request. + private readonly int _batchSize; + + // The interval at which the background timer attempts to reprocess any failed documents in the store. + private readonly TimeSpan _retryInterval; + + // Timer for retrying previously failed documents. + private System.Timers.Timer _retryTimer; + + // Flush interval settings + private readonly TimeSpan _flushInterval; + private System.Timers.Timer _flushTimer; + + // Flag indicating operator is started (to handle concurrency safely). + private volatile bool _isStarted; + + /// + /// Creates a new Elasticsearch Sink Operator using the 8.x client. + /// + /// + /// The configured ElasticsearchClient used to send data to Elasticsearch. + /// + /// + /// The name of the index to which documents should be written. + /// + /// + /// An IDataStore used to store any documents that fail to index. + /// They will be retried later automatically. + /// + /// + /// An optional logger for information and error messages. If null, logs go to Console. + /// + /// + /// Optional batch size. When the in-flight buffer hits this size, a bulk request is triggered. + /// Defaults to 50. + /// + /// + /// How often we retry failed documents from the IDataStore. + /// Defaults to 60 seconds. + /// + public ElasticsearchSinkOperator( + ElasticsearchClient client, + string indexName, + int batchSize = 50, + TimeSpan? flushInterval = null, + TimeSpan? retryInterval = null, + IDataStore failedDocumentsStore = null, + ILogger> logger = null) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _indexName = indexName ?? throw new ArgumentNullException(nameof(indexName)); + + _failedDocumentsStore = failedDocumentsStore ?? new InMemoryStateStore("default_failedDocuments"); + + _logger = logger; + + _batchSize = batchSize; + _retryInterval = retryInterval ?? TimeSpan.FromSeconds(60); + + // If flushInterval is not set or set to TimeSpan.Zero, the flush-timer won't be started. + _flushInterval = flushInterval ?? TimeSpan.Zero; + + _currentBatch = new List(); + } + + /// + /// Called by the pipeline to process each record. + /// Accumulates into a batch, then flushes to ES once we exceed _batchSize. + /// + public void Process(TInput input) + { + if (!_isStarted) + { + LogError("Process called before the ElasticsearchSinkOperator was started. Please start the operator before start processing."); + } + + lock (_batchLock) + { + _currentBatch.Add(input); + + if (_currentBatch.Count >= _batchSize) + { + FlushBatch(_currentBatch); + _currentBatch.Clear(); + } + } + } + + /// + /// Start the operator, initializing any resources or background tasks if needed. + /// + public void Start() + { + if (_isStarted) return; + + _isStarted = true; + LogInformation($"ElasticsearchSinkOperator started. Will retry failed docs every {_retryInterval}."); + + // Create a timer to periodically retry failed documents in the store. + _retryTimer = new System.Timers.Timer(_retryInterval.TotalMilliseconds); + _retryTimer.Elapsed += (s, e) => RetryFailedDocuments(); + _retryTimer.AutoReset = true; + _retryTimer.Start(); + + // If flushInterval is set (> 0), set up a flush timer as well. + if (_flushInterval > TimeSpan.Zero) + { + LogInformation($"Flush interval set to {_flushInterval}. Will flush partial batches on this schedule."); + _flushTimer = new System.Timers.Timer(_flushInterval.TotalMilliseconds); + _flushTimer.Elapsed += (s, e) => FlushPendingBatch(); + _flushTimer.AutoReset = true; + _flushTimer.Start(); + } + } + + /// + /// Stop the operator, flushing any in-memory data and disposing of resources. + /// + public void Stop() + { + if (!_isStarted) return; + + _isStarted = false; + LogInformation("ElasticsearchSinkOperator stopping."); + + // Stop the background timers and dispose them. + _retryTimer?.Stop(); + _retryTimer?.Dispose(); + + _flushTimer?.Stop(); + _flushTimer?.Dispose(); + + // Flush any remaining documents in the current batch. + FlushPendingBatch(); + + LogInformation("ElasticsearchSinkOperator stopped."); + } + + /// + /// Flush the current batch if there are any items present. + /// + private void FlushPendingBatch() + { + lock (_batchLock) + { + if (_currentBatch.Count > 0) + { + LogInformation($"FlushPendingBatch triggered. Current batch size: {_currentBatch.Count}."); + FlushBatch(_currentBatch); + _currentBatch.Clear(); + } + } + } + + /// + /// Flush a batch of documents to Elasticsearch in bulk. + /// Any failed items are persisted into the IDataStore for later retry. + /// + private void FlushBatch(IList batch) + { + if (batch.Count == 0) return; + + LogInformation($"Flushing batch of size {batch.Count} to Elasticsearch index '{_indexName}'."); + + // Build a BulkRequest with index operations: + var bulkRequest = new BulkRequest(_indexName) + { + Operations = new List() + }; + + foreach (var doc in batch) + { + bulkRequest.Operations.Add(new BulkIndexOperation(doc)); + } + + var response = _client.BulkAsync(bulkRequest).Result; + + // Check top-level success + if (!response.IsSuccess()) + { + var errorMsg = $"Bulk request failed entirely: {response.DebugInformation}"; + LogError(errorMsg); + + // Store everything in the retry store. + foreach (var doc in batch) + { + _failedDocumentsStore.Put(Guid.NewGuid().ToString(), doc); + } + return; + } + + // If top-level succeeded, check for partial failures + if (response.Errors) + { + foreach (var item in response.Items) + { + if (item.Status >= 300) // e.g., 400, 404, 500... + { + var reason = item.Error?.Reason ?? "Unknown reason"; + LogError($"Partial failure for item. Status={item.Status}, Reason={reason}"); + + // For robust correlation, track item.Id or external ID mapping. + var failedDoc = batch.FirstOrDefault(); + _failedDocumentsStore.Put(Guid.NewGuid().ToString(), failedDoc); + } + } + } + } + + /// + /// Attempts to reindex documents that previously failed. + /// + private void RetryFailedDocuments() + { + var allFailed = _failedDocumentsStore.GetAll().ToList(); + if (!allFailed.Any()) return; + + LogInformation($"Retrying {allFailed.Count} failed documents..."); + + var bulkRequest = new BulkRequest(_indexName) + { + Operations = new List() + }; + + // Correlate each operation with its store key. + // This lets us remove only those that succeed. + foreach (var (storeKey, doc) in allFailed) + { + var indexOperation = new BulkIndexOperation(doc) + { + Id = storeKey // correlation ID for partial success/failure checks + }; + bulkRequest.Operations.Add(indexOperation); + } + + var response = _client.BulkAsync(bulkRequest).Result; + + if (!response.IsSuccess()) + { + var errorMsg = $"Bulk retry request failed: {response.DebugInformation}"; + LogError(errorMsg); + return; + } + + // If bulk is overall successful, we still check items for partial failures. + if (response.Errors) + { + foreach (var item in response.Items) + { + if (item.Status < 300) + { + // Successfully reindexed; remove from store. + _failedDocumentsStore.Remove(item.Id); + } + else + { + // Partial failure; doc remains in store for next retry. + LogError($"Retry partial failure: Status={item.Status}, Reason={item.Error?.Reason}"); + } + } + } + else + { + // If there were no errors at all, remove every doc we retried. + foreach (var (storeKey, _) in allFailed) + { + _failedDocumentsStore.Remove(storeKey); + } + } + } + + // -------------------------------------------------------------------- + // LOGGING HELPERS + // -------------------------------------------------------------------- + private void LogInformation(string message) + { + if (_logger != null) + { + _logger.LogInformation(message); + } + else + { + Console.WriteLine(message); + } + } + + private void LogError(string message, Exception ex = null) + { + if (_logger != null) + { + _logger.LogError(ex, message); + } + else + { + Console.WriteLine(ex != null + ? $"ERROR: {message}\n{ex}" + : $"ERROR: {message}"); + } + } + + /// + /// Implementation of IStatefulOperator for exposing the store(s). + /// + public IEnumerable GetStateStores() + { + // Return any data stores that we rely on for statefulness. + return new[] { _failedDocumentsStore }; + } + } +} diff --git a/src/Cortex.Streams.Files/Assets/cortex.png b/src/Cortex.Streams.Files/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.Files/Assets/cortex.png and b/src/Cortex.Streams.Files/Assets/cortex.png differ diff --git a/src/Cortex.Streams.Http/Assets/cortex.png b/src/Cortex.Streams.Http/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.Http/Assets/cortex.png and b/src/Cortex.Streams.Http/Assets/cortex.png differ diff --git a/src/Cortex.Streams.Kafka/Assets/cortex.png b/src/Cortex.Streams.Kafka/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.Kafka/Assets/cortex.png and b/src/Cortex.Streams.Kafka/Assets/cortex.png differ diff --git a/src/Cortex.Streams.MSSqlServer/Assets/cortex.png b/src/Cortex.Streams.MSSqlServer/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.MSSqlServer/Assets/cortex.png and b/src/Cortex.Streams.MSSqlServer/Assets/cortex.png differ diff --git a/src/Cortex.Streams.MongoDb/Assets/cortex.png b/src/Cortex.Streams.MongoDb/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.MongoDb/Assets/cortex.png and b/src/Cortex.Streams.MongoDb/Assets/cortex.png differ diff --git a/src/Cortex.Streams.PostgreSQL/Assets/cortex.png b/src/Cortex.Streams.PostgreSQL/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.PostgreSQL/Assets/cortex.png and b/src/Cortex.Streams.PostgreSQL/Assets/cortex.png differ diff --git a/src/Cortex.Streams.Pulsar/Assets/cortex.png b/src/Cortex.Streams.Pulsar/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.Pulsar/Assets/cortex.png and b/src/Cortex.Streams.Pulsar/Assets/cortex.png differ diff --git a/src/Cortex.Streams.RabbitMQ/Assets/cortex.png b/src/Cortex.Streams.RabbitMQ/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.RabbitMQ/Assets/cortex.png and b/src/Cortex.Streams.RabbitMQ/Assets/cortex.png differ diff --git a/src/Cortex.Streams.S3/Assets/cortex.png b/src/Cortex.Streams.S3/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams.S3/Assets/cortex.png and b/src/Cortex.Streams.S3/Assets/cortex.png differ diff --git a/src/Cortex.Streams/Assets/cortex.png b/src/Cortex.Streams/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Streams/Assets/cortex.png and b/src/Cortex.Streams/Assets/cortex.png differ diff --git a/src/Cortex.Telemetry.OpenTelemetry/Assets/cortex.png b/src/Cortex.Telemetry.OpenTelemetry/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Telemetry.OpenTelemetry/Assets/cortex.png and b/src/Cortex.Telemetry.OpenTelemetry/Assets/cortex.png differ diff --git a/src/Cortex.Telemetry/Assets/cortex.png b/src/Cortex.Telemetry/Assets/cortex.png index a4f9727..101a1fb 100644 Binary files a/src/Cortex.Telemetry/Assets/cortex.png and b/src/Cortex.Telemetry/Assets/cortex.png differ diff --git a/src/Cortex.Types/AllOf/AllOf1.cs b/src/Cortex.Types/AllOf/AllOf1.cs new file mode 100644 index 0000000..441ad62 --- /dev/null +++ b/src/Cortex.Types/AllOf/AllOf1.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that is all of the specified types. The value must be compatible with each of the type parameters. + /// + /// First required type + /// Second required type + public readonly struct AllOf : IEquatable>, IAllOf + { + private readonly object _value; + + /// + public object Value => _value; + + private AllOf(object value) + { + if (!(value is T1)) + throw new ArgumentException($"Value must be compatible with {typeof(T1).Name}."); + _value = value; + } + + /// + /// Creates an AllOf instance from a value that is compatible with all type parameters. + /// + /// Type of the value which must implement all type parameters + public static AllOf Create(T value) where T : T1 => new AllOf(value); + + + // For now we are skipping the implicit operations + //public static implicit operator AllOf(T1 value) + //{ + // if (value is T1) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T1).Name} is not compatible."); + //} + + + + public bool Is() => _value is T; + + public T As() + { + if (_value is T val) return val; + throw new InvalidCastException($"Cannot cast {_value?.GetType().Name} to {typeof(T).Name}."); + } + + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + result = default!; + return false; + } + + public bool Equals(AllOf other) => Equals(_value, other._value); + public override bool Equals(object obj) => obj is AllOf other && Equals(other); + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public static bool operator ==(AllOf left, AllOf right) => left.Equals(right); + public static bool operator !=(AllOf left, AllOf right) => !left.Equals(right); + + public override string ToString() => _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AllOf/AllOf2.cs b/src/Cortex.Types/AllOf/AllOf2.cs new file mode 100644 index 0000000..52ee742 --- /dev/null +++ b/src/Cortex.Types/AllOf/AllOf2.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that is all of the specified types. The value must be compatible with each of the type parameters. + /// + /// First required type + /// Second required type + public readonly struct AllOf : IEquatable>, IAllOf + { + private readonly object _value; + + /// + public object Value => _value; + + private AllOf(object value) + { + if (!(value is T1) || !(value is T2)) + throw new ArgumentException($"Value must be compatible with {typeof(T1).Name} and {typeof(T2).Name}."); + _value = value; + } + + /// + /// Creates an AllOf instance from a value that is compatible with all type parameters. + /// + /// Type of the value which must implement all type parameters + public static AllOf Create(T value) where T : T1, T2 => new AllOf(value); + + + // For now we are skipping the implicit operations + //public static implicit operator AllOf(T1 value) + //{ + // if (value is T2) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T1).Name} is not compatible with {typeof(T2).Name}."); + //} + + //public static implicit operator AllOf(T2 value) + //{ + // if (value is T1) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T2).Name} is not compatible with {typeof(T1).Name}."); + //} + + public bool Is() => _value is T; + + public T As() + { + if (_value is T val) return val; + throw new InvalidCastException($"Cannot cast {_value?.GetType().Name} to {typeof(T).Name}."); + } + + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + result = default!; + return false; + } + + public bool Equals(AllOf other) => Equals(_value, other._value); + public override bool Equals(object obj) => obj is AllOf other && Equals(other); + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public static bool operator ==(AllOf left, AllOf right) => left.Equals(right); + public static bool operator !=(AllOf left, AllOf right) => !left.Equals(right); + + public override string ToString() => _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AllOf/AllOf3.cs b/src/Cortex.Types/AllOf/AllOf3.cs new file mode 100644 index 0000000..005dc0c --- /dev/null +++ b/src/Cortex.Types/AllOf/AllOf3.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that is all of the specified types. The value must be compatible with each of the type parameters. + /// + /// First required type + /// Second required type + /// Third required type + public readonly struct AllOf : IEquatable>, IAllOf + { + private readonly object _value; + + /// + public object Value => _value; + + private AllOf(object value) + { + if (!(value is T1) || !(value is T2) || !(value is T3)) + throw new ArgumentException($"Value must be compatible with {typeof(T1).Name}, {typeof(T2).Name}, and {typeof(T3).Name}."); + _value = value; + } + + /// + /// Creates an AllOf instance from a value that is compatible with all type parameters. + /// + /// Type of the value which must implement all type parameters + public static AllOf Create(T value) where T : T1, T2, T3 => new AllOf(value); + + + // For now we are skipping the implicit operations + //public static implicit operator AllOf(T1 value) + //{ + // if (value is T2 && value is T3) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T1).Name} is not compatible with all required types."); + //} + + //public static implicit operator AllOf(T2 value) + //{ + // if (value is T1 && value is T3) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T2).Name} is not compatible with all required types."); + //} + + //public static implicit operator AllOf(T3 value) + //{ + // if (value is T1 && value is T2) + // return new AllOf(value); + // throw new InvalidCastException($"{typeof(T3).Name} is not compatible with all required types."); + //} + + public bool Is() => _value is T; + + public T As() + { + if (_value is T val) return val; + throw new InvalidCastException($"Cannot cast {_value?.GetType().Name} to {typeof(T).Name}."); + } + + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + result = default!; + return false; + } + + public bool Equals(AllOf other) => Equals(_value, other._value); + public override bool Equals(object obj) => obj is AllOf other && Equals(other); + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public static bool operator ==(AllOf left, AllOf right) => left.Equals(right); + public static bool operator !=(AllOf left, AllOf right) => !left.Equals(right); + + public override string ToString() => _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AllOf/IAllOf.cs b/src/Cortex.Types/AllOf/IAllOf.cs new file mode 100644 index 0000000..f72c6e8 --- /dev/null +++ b/src/Cortex.Types/AllOf/IAllOf.cs @@ -0,0 +1,13 @@ +namespace Cortex.Types +{ + /// + /// Base interface for all AllOf types providing common functionality + /// + public interface IAllOf + { + /// + /// Gets the boxed value stored in the AllOf container + /// + object Value { get; } + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf2.cs b/src/Cortex.Types/AnyOf/AnyOf2.cs new file mode 100644 index 0000000..bfa0f1c --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf2.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of the specified types + /// + /// First possible type + /// Second possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => + new(value, new HashSet { 0 }); + + public static implicit operator AnyOf(T2 value) => + new(value, new HashSet { 1 }); + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler) => _typeIndices.Contains(0) && _value is T1 t1 + ? t1Handler(t1) + : _typeIndices.Contains(1) && _value is T2 t2 + ? t2Handler(t2) + : throw new InvalidOperationException("Invalid state"); + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) + { + t1Action(t1); + } + else if (_typeIndices.Contains(1) && _value is T2 t2) + { + t2Action(t2); + } + else + { + throw new InvalidOperationException("Invalid state"); + } + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + } + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf3.cs b/src/Cortex.Types/AnyOf/AnyOf3.cs new file mode 100644 index 0000000..101bbf4 --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf3.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of the specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => + new(value, new HashSet { 0 }); + + public static implicit operator AnyOf(T2 value) => + new(value, new HashSet { 1 }); + + public static implicit operator AnyOf(T3 value) => + new(value, new HashSet { 2 }); + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler) => _typeIndices.Contains(0) && _value is T1 t1 + ? t1Handler(t1) + : _typeIndices.Contains(1) && _value is T2 t2 + ? t2Handler(t2) + : _typeIndices.Contains(2) && _value is T3 t3 + ? t3Handler(t3) + : throw new InvalidOperationException("Invalid state"); + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) + { + t1Action(t1); + } + else if (_typeIndices.Contains(1) && _value is T2 t2) + { + t2Action(t2); + } + else if (_typeIndices.Contains(2) && _value is T3 t3) + { + t3Action(t3); + } + else + { + throw new InvalidOperationException("Invalid state"); + } + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf4.cs b/src/Cortex.Types/AnyOf/AnyOf4.cs new file mode 100644 index 0000000..b87a295 --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf4.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of the specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => + new(value, new HashSet { 0 }); + + public static implicit operator AnyOf(T2 value) => + new(value, new HashSet { 1 }); + + public static implicit operator AnyOf(T3 value) => + new(value, new HashSet { 2 }); + + public static implicit operator AnyOf(T4 value) => + new(value, new HashSet { 3 }); + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler) => _typeIndices.Contains(0) && _value is T1 t1 + ? t1Handler(t1) + : _typeIndices.Contains(1) && _value is T2 t2 + ? t2Handler(t2) + : _typeIndices.Contains(2) && _value is T3 t3 + ? t3Handler(t3) + : _typeIndices.Contains(3) && _value is T4 t4 + ? t4Handler(t4) + : throw new InvalidOperationException("Invalid state"); + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) + { + t1Action(t1); + } + else if (_typeIndices.Contains(1) && _value is T2 t2) + { + t2Action(t2); + } + else if (_typeIndices.Contains(2) && _value is T3 t3) + { + t3Action(t3); + } + else if (_typeIndices.Contains(3) && _value is T4 t4) + { + t4Action(t4); + } + else + { + throw new InvalidOperationException("Invalid state"); + } + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + if (_value is T4) yield return typeof(T4); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/IAnyOf.cs b/src/Cortex.Types/AnyOf/IAnyOf.cs new file mode 100644 index 0000000..ec5a81d --- /dev/null +++ b/src/Cortex.Types/AnyOf/IAnyOf.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Cortex.Types +{ + /// + /// Base interface for all AnyOf types providing common functionality + /// + public interface IAnyOf + { + /// + /// Gets the boxed value stored in the AnyOf container + /// + object Value { get; } + + /// + /// Gets the indices of the declared type parameters that match the stored value + /// + IEnumerable TypeIndices { get; } + } +} diff --git a/src/Cortex.Types/Assets/cortex.png b/src/Cortex.Types/Assets/cortex.png new file mode 100644 index 0000000..101a1fb Binary files /dev/null and b/src/Cortex.Types/Assets/cortex.png differ diff --git a/src/Cortex.Types/Assets/license.md b/src/Cortex.Types/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Types/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Types/Cortex.Types.csproj b/src/Cortex.Types/Cortex.Types.csproj new file mode 100644 index 0000000..9e6c1ac --- /dev/null +++ b/src/Cortex.Types/Cortex.Types.csproj @@ -0,0 +1,53 @@ + + + + net9.0;net8.0;net7.0;netstandard2.1 + 9 + + 1.0.1 + 1.0.1 + Buildersoft Cortex Framework + Buildersoft + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + Cortex Data Framework is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. + + + https://github.com/buildersoftio/cortex + cortex types oneOf allOf anyOff not + + 1.0.1 + license.md + cortex.png + Cortex.Types + True + True + True + git + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + Cortex Data Framework + README.md + + + + + + True + \ + + + True + + + + True + + + + + + + + diff --git a/src/Cortex.Types/OneOf/IOneOf.cs b/src/Cortex.Types/OneOf/IOneOf.cs new file mode 100644 index 0000000..c28e667 --- /dev/null +++ b/src/Cortex.Types/OneOf/IOneOf.cs @@ -0,0 +1,18 @@ +namespace Cortex.Types +{ + /// + /// Base interface for all OneOf types providing common functionality + /// + public interface IOneOf + { + /// + /// Gets the boxed value stored in the OneOf container + /// + object Value { get; } + + /// + /// Gets the 0-based index of the declared type parameter + /// + int TypeIndex { get; } + } +} diff --git a/src/Cortex.Types/OneOf/OneOf2.cs b/src/Cortex.Types/OneOf/OneOf2.cs new file mode 100644 index 0000000..40abbcc --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf2.cs @@ -0,0 +1,139 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of two specified types + /// + /// First possible type + /// Second possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => + new(value, 0); + public static implicit operator OneOf(T2 value) => + new(value, 1); + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + /// + /// + /// OneOf<Exception, string> value = new ArgumentException(); + /// value.Is<Exception>(); // true + /// value.Is<ArgumentException>(); // true + /// value.Is<string>(); // false + /// + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + /// + /// + /// OneOf<int, string> value = "42"; + /// string s = value.As<string>(); // "42" + /// int i = value.As<int>(); // throws + /// + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + /// + /// + /// if (value.TryGet(out string s)) { + /// Console.WriteLine(s); + /// } + /// + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} " + + $"to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf3.cs b/src/Cortex.Types/OneOf/OneOf3.cs new file mode 100644 index 0000000..16d67c3 --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf3.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of three specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + /// + /// + /// OneOf<Exception, string> value = new ArgumentException(); + /// value.Is<Exception>(); // true + /// value.Is<ArgumentException>(); // true + /// value.Is<string>(); // false + /// + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + /// + /// + /// OneOf<int, string> value = "42"; + /// string s = value.As<string>(); // "42" + /// int i = value.As<int>(); // throws + /// + /// + public T As() => _value is T val ? val : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + /// + /// + /// if (value.TryGet(out string s)) { + /// Console.WriteLine(s); + /// } + /// + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf4.cs b/src/Cortex.Types/OneOf/OneOf4.cs new file mode 100644 index 0000000..e40c4b5 --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf4.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of three specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Forth possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + public static implicit operator OneOf(T4 value) => new(value, 3); + + + /// + /// Checks if the contained value is of or derived from type + /// + /// Type to check against + /// + /// True if value is exactly or derived from it + /// + /// + /// + /// OneOf<Exception, string> value = new ArgumentException(); + /// value.Is<Exception>(); // true + /// value.Is<ArgumentException>(); // true + /// value.Is<string>(); // false + /// + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// Target type + /// + /// Thrown when value is not compatible with + /// + /// + /// + /// OneOf<int, string> value = "42"; + /// string s = value.As<string>(); // "42" + /// int i = value.As<int>(); // throws + /// + /// + public T As() => _value is T val ? val : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + + /// + /// Attempts to retrieve the value as + /// + /// Out parameter receiving the value if successful + /// True if value is compatible with + /// + /// + /// if (value.TryGet(out string s)) { + /// Console.WriteLine(s); + /// } + /// + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + 3 => t4Handler((T4)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + case 3: t4Action((T4)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +}