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