diff --git a/Foundation.Data.Doublets.Cli.Tests/StorablePatternManagerTests.cs b/Foundation.Data.Doublets.Cli.Tests/StorablePatternManagerTests.cs new file mode 100644 index 0000000..3919271 --- /dev/null +++ b/Foundation.Data.Doublets.Cli.Tests/StorablePatternManagerTests.cs @@ -0,0 +1,152 @@ +using Xunit; +using Foundation.Data.Doublets.Cli; +using System.IO; +using System.Text.Json; + +namespace Foundation.Data.Doublets.Cli.Tests +{ + public class StorablePatternManagerTests + { + private readonly string _tempFilePath; + + public StorablePatternManagerTests() + { + _tempFilePath = Path.GetTempFileName(); + } + + public void Dispose() + { + if (File.Exists(_tempFilePath)) + { + File.Delete(_tempFilePath); + } + } + + [Fact] + public void AddPattern_ShouldStorePatternCorrectly() + { + // Arrange + var manager = new StorablePatternManager(_tempFilePath); + var query = "((1 1)) ((1 2))"; + + // Act + manager.AddPattern(query, "Test pattern"); + + // Assert + var patterns = manager.GetActivePatterns(); + Assert.Single(patterns); + Assert.Equal(query, patterns[0].Query); + Assert.Equal("Test pattern", patterns[0].Description); + Assert.True(patterns[0].IsActive); + } + + [Fact] + public void RemovePattern_ShouldRemoveExistingPattern() + { + // Arrange + var manager = new StorablePatternManager(_tempFilePath); + var query = "((1 1)) ((1 2))"; + manager.AddPattern(query); + + // Act + bool removed = manager.RemovePattern(query); + + // Assert + Assert.True(removed); + var patterns = manager.GetActivePatterns(); + Assert.Empty(patterns); + } + + [Fact] + public void RemovePattern_ShouldReturnFalseForNonExistentPattern() + { + // Arrange + var manager = new StorablePatternManager(_tempFilePath); + var query = "((1 1)) ((1 2))"; + + // Act + bool removed = manager.RemovePattern(query); + + // Assert + Assert.False(removed); + } + + [Fact] + public void GetActivePatterns_ShouldReturnOnlyActivePatterns() + { + // Arrange + var manager = new StorablePatternManager(_tempFilePath); + manager.AddPattern("((1 1)) ((1 2))", "Pattern 1"); + manager.AddPattern("((2 2)) ((2 3))", "Pattern 2"); + + // Act + var patterns = manager.GetActivePatterns(); + + // Assert + Assert.Equal(2, patterns.Count); + Assert.All(patterns, p => Assert.True(p.IsActive)); + } + + [Fact] + public void RemoveAllPatterns_ShouldClearAllPatterns() + { + // Arrange + var manager = new StorablePatternManager(_tempFilePath); + manager.AddPattern("((1 1)) ((1 2))"); + manager.AddPattern("((2 2)) ((2 3))"); + + // Act + manager.RemoveAllPatterns(); + + // Assert + var patterns = manager.GetActivePatterns(); + Assert.Empty(patterns); + } + + [Fact] + public void LoadPatterns_ShouldHandleNonExistentFile() + { + // Arrange + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + // Act & Assert (should not throw) + var manager = new StorablePatternManager(nonExistentPath); + var patterns = manager.GetActivePatterns(); + Assert.Empty(patterns); + } + + [Fact] + public void LoadPatterns_ShouldHandleCorruptedFile() + { + // Arrange + File.WriteAllText(_tempFilePath, "invalid json content"); + + // Act & Assert (should not throw) + var manager = new StorablePatternManager(_tempFilePath); + var patterns = manager.GetActivePatterns(); + Assert.Empty(patterns); + } + + [Fact] + public void PersistenceTest_ShouldMaintainPatternsAcrossInstances() + { + // Arrange + var query1 = "((1 1)) ((1 2))"; + var query2 = "((2 2)) ((2 3))"; + + // Act - First instance + var manager1 = new StorablePatternManager(_tempFilePath); + manager1.AddPattern(query1, "Pattern 1"); + manager1.AddPattern(query2, "Pattern 2"); + + // Act - Second instance + var manager2 = new StorablePatternManager(_tempFilePath); + var patterns = manager2.GetActivePatterns(); + + // Assert + Assert.Equal(2, patterns.Count); + Assert.Contains(patterns, p => p.Query == query1); + Assert.Contains(patterns, p => p.Query == query2); + } + } +} \ No newline at end of file diff --git a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj index 1d8f9ab..e4d6c4d 100644 --- a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj +++ b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj @@ -15,7 +15,7 @@ link-foundation A CLI tool for links manipulation. clink - 2.2.2 + 2.3.0 Unlicense https://github.com/link-foundation/link-cli diff --git a/Foundation.Data.Doublets.Cli/Program.cs b/Foundation.Data.Doublets.Cli/Program.cs index 1f9bfed..e035d9e 100644 --- a/Foundation.Data.Doublets.Cli/Program.cs +++ b/Foundation.Data.Doublets.Cli/Program.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using System.CommandLine.Invocation; using Platform.Data; using Platform.Data.Doublets; using Platform.Data.Doublets.Memory.United.Generic; @@ -70,6 +71,24 @@ afterOption.AddAlias("--links"); afterOption.AddAlias("-a"); +var alwaysOption = new Option( + name: "--always", + description: "Store the query as a transformation pattern to be executed on every data store change", + getDefaultValue: () => false +); + +var neverOption = new Option( + name: "--never", + description: "Remove the stored transformation pattern", + getDefaultValue: () => false +); + +var patternsFileOption = new Option( + name: "--patterns-file", + description: "Path to the transformation patterns file", + getDefaultValue: () => "patterns.json" +); + var rootCommand = new RootCommand("LiNo CLI Tool for managing links data store") { dbOption, @@ -79,12 +98,26 @@ structureOption, beforeOption, changesOption, - afterOption + afterOption, + alwaysOption, + neverOption, + patternsFileOption }; rootCommand.SetHandler( - (string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) => + (InvocationContext context) => { + var db = context.ParseResult.GetValueForOption(dbOption)!; + var queryOptionValue = context.ParseResult.GetValueForOption(queryOption) ?? ""; + var queryArgumentValue = context.ParseResult.GetValueForArgument(queryArgument) ?? ""; + var trace = context.ParseResult.GetValueForOption(traceOption); + var structure = context.ParseResult.GetValueForOption(structureOption); + var before = context.ParseResult.GetValueForOption(beforeOption); + var changes = context.ParseResult.GetValueForOption(changesOption); + var after = context.ParseResult.GetValueForOption(afterOption); + var always = context.ParseResult.GetValueForOption(alwaysOption); + var never = context.ParseResult.GetValueForOption(neverOption); + var patternsFile = context.ParseResult.GetValueForOption(patternsFileOption)!; var decoratedLinks = new NamedLinksDecorator(db, trace); // If --structure is provided, handle it separately @@ -141,13 +174,45 @@ } } + // Handle storable transformation patterns + var patternManager = new StorablePatternManager(patternsFile); + + if (always && !string.IsNullOrWhiteSpace(effectiveQuery)) + { + patternManager.AddPattern(effectiveQuery); + Console.WriteLine($"Transformation pattern stored: {effectiveQuery}"); + } + + if (never && !string.IsNullOrWhiteSpace(effectiveQuery)) + { + bool removed = patternManager.RemovePattern(effectiveQuery); + if (removed) + { + Console.WriteLine($"Transformation pattern removed: {effectiveQuery}"); + } + else + { + Console.WriteLine($"Transformation pattern not found: {effectiveQuery}"); + } + } + + // Apply stored patterns on any data change + if (changesList.Any()) + { + patternManager.ApplyStoredPatternsOnChange(decoratedLinks, (status, pattern) => + { + if (trace) + { + Console.WriteLine($"Applied stored pattern: {pattern.Query} - {status}"); + } + }); + } + if (after) { PrintAllLinks(decoratedLinks); } - }, - // Explicitly specify the type parameters - dbOption, queryOption, queryArgument, traceOption, structureOption, beforeOption, changesOption, afterOption + } ); await rootCommand.InvokeAsync(args); diff --git a/Foundation.Data.Doublets.Cli/StorablePatternManager.cs b/Foundation.Data.Doublets.Cli/StorablePatternManager.cs new file mode 100644 index 0000000..8e57c28 --- /dev/null +++ b/Foundation.Data.Doublets.Cli/StorablePatternManager.cs @@ -0,0 +1,121 @@ +using System.Text.Json; + +namespace Foundation.Data.Doublets.Cli +{ + public class StorablePatternManager + { + private readonly string _patternFilePath; + private readonly List _patterns; + + public StorablePatternManager(string patternFilePath) + { + _patternFilePath = patternFilePath; + _patterns = LoadPatterns(); + } + + public void AddPattern(string query, string? description = null) + { + var pattern = new StoredPattern + { + Id = Guid.NewGuid().ToString(), + Query = query, + Description = description, + CreatedAt = DateTime.UtcNow, + IsActive = true + }; + + _patterns.Add(pattern); + SavePatterns(); + } + + public bool RemovePattern(string query) + { + var pattern = _patterns.FirstOrDefault(p => p.Query == query); + if (pattern != null) + { + _patterns.Remove(pattern); + SavePatterns(); + return true; + } + return false; + } + + public void RemoveAllPatterns() + { + _patterns.Clear(); + SavePatterns(); + } + + public IReadOnlyList GetActivePatterns() + { + return _patterns.Where(p => p.IsActive).ToList().AsReadOnly(); + } + + public void ApplyStoredPatternsOnChange(NamedLinksDecorator links, Action onPatternExecution) + { + var activePatterns = GetActivePatterns(); + foreach (var pattern in activePatterns) + { + try + { + var options = new AdvancedMixedQueryProcessor.Options + { + Query = pattern.Query, + Trace = false + }; + + AdvancedMixedQueryProcessor.ProcessQuery(links, options); + onPatternExecution?.Invoke("Success", pattern); + } + catch (Exception ex) + { + onPatternExecution?.Invoke($"Error: {ex.Message}", pattern); + } + } + } + + private List LoadPatterns() + { + if (!File.Exists(_patternFilePath)) + { + return new List(); + } + + try + { + var json = File.ReadAllText(_patternFilePath); + var patterns = JsonSerializer.Deserialize>(json); + return patterns ?? new List(); + } + catch (Exception) + { + return new List(); + } + } + + private void SavePatterns() + { + try + { + var json = JsonSerializer.Serialize(_patterns, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(_patternFilePath, json); + } + catch (Exception) + { + // Log or handle error as needed + } + } + } + + public class StoredPattern + { + public string Id { get; set; } = string.Empty; + public string Query { get; set; } = string.Empty; + public string? Description { get; set; } + public DateTime CreatedAt { get; set; } + public bool IsActive { get; set; } = true; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 9399258..e241fae 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,63 @@ clink '((($index: $source $target)) (($index: $target $source)))' --changes --af clink '((1: 2 1) (2: 1 2)) ()' --changes --after ``` +## Storable Transformation Patterns + +You can store transformation patterns to be automatically executed whenever data changes occur. This enables dynamic programmable behavior for the links network. + +### Store a transformation pattern + +Store a transformation pattern that changes any link with source 1 and target 1 to have target 2: + +```bash +clink '((1 1)) ((1 2))' --always --changes +``` +→ +``` +((1: 1 1)) ((1: 1 2)) +Transformation pattern stored: ((1 1)) ((1 2)) +``` + +The pattern is saved in `patterns.json` file and will be automatically applied whenever any data change occurs. + +### Trigger stored patterns + +When you make any change to the data store, stored patterns are automatically executed: + +```bash +clink '() ((1 1))' --changes --after +``` +→ +``` +() ((1: 1 1)) +Applied stored pattern: ((1 1)) ((1 2)) - Success +(1: 1 2) +``` + +Notice how the link was created as `(1 1)` but the stored pattern transformed it to `(1 2)`. + +### Remove a transformation pattern + +Remove a stored transformation pattern: + +```bash +clink '((1 1)) ((1 2))' --never +``` +→ +``` +Transformation pattern removed: ((1 1)) ((1 2)) +``` + +### Use custom patterns file + +Store patterns in a separate file to avoid making the links network dynamically programmable: + +```bash +clink '((2 2)) ((2 3))' --always --patterns-file my_patterns.json +``` + +This creates a separate `my_patterns.json` file for storing transformation patterns. + ## All options and arguments | Parameter | Type | Default Value | Aliases | Description | @@ -227,6 +284,9 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after | `--before` | bool | `false` | `-b` | Print the state of the database before applying changes | | `--changes` | bool | `false` | `-c` | Print the changes applied by the query | | `--after` | bool | `false` | `--links`, `-a` | Print the state of the database after applying changes | +| `--always` | bool | `false` | _None_ | Store the query as a transformation pattern to be executed on every data store change | +| `--never` | bool | `false` | _None_ | Remove the stored transformation pattern | +| `--patterns-file` | string | `patterns.json` | _None_ | Path to the transformation patterns file | ## For developers and debugging diff --git a/examples/test_storable_patterns.sh b/examples/test_storable_patterns.sh new file mode 100755 index 0000000..488bada --- /dev/null +++ b/examples/test_storable_patterns.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Clean up any existing test files +rm -f test_db.links test_patterns.json + +echo "=== Testing Storable Transformation Patterns ===" + +echo "" +echo "1. Create initial links" +dotnet run --project Foundation.Data.Doublets.Cli -- '() ((1 1) (2 2))' --db test_db.links --changes --after + +echo "" +echo "2. Store a transformation pattern using --always" +dotnet run --project Foundation.Data.Doublets.Cli -- '((1: 1 1)) ((1: 1 3))' --db test_db.links --always --patterns-file test_patterns.json --changes + +echo "" +echo "3. Check if patterns.json file was created and contains the pattern" +if [ -f test_patterns.json ]; then + echo "Patterns file created:" + cat test_patterns.json +else + echo "ERROR: Patterns file not found" +fi + +echo "" +echo "4. Make a change to trigger stored patterns" +dotnet run --project Foundation.Data.Doublets.Cli -- '() ((3 3))' --db test_db.links --patterns-file test_patterns.json --changes --after --trace + +echo "" +echo "5. Remove the stored pattern using --never" +dotnet run --project Foundation.Data.Doublets.Cli -- '((1: 1 1)) ((1: 1 3))' --db test_db.links --never --patterns-file test_patterns.json + +echo "" +echo "6. Check if pattern was removed" +if [ -f test_patterns.json ]; then + echo "Patterns file after removal:" + cat test_patterns.json +else + echo "Patterns file not found" +fi + +echo "" +echo "=== Test completed ===" \ No newline at end of file